From 345a6179132174da1ce831e0d0872e0390e7bdcf Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Wed, 1 May 2024 19:43:35 +0000 Subject: [PATCH 001/399] feat(api): update via SDK Studio --- .devcontainer/Dockerfile | 9 + .devcontainer/devcontainer.json | 40 + .github/workflows/ci.yml | 62 + .gitignore | 15 + .python-version | 1 + .stats.yml | 2 + Brewfile | 2 + CONTRIBUTING.md | 125 ++ LICENSE | 201 ++ README.md | 392 +++- api.md | 35 + bin/publish-pypi | 6 + examples/.keep | 4 + mypy.ini | 47 + noxfile.py | 9 + pyproject.toml | 196 ++ requirements-dev.lock | 96 + requirements.lock | 43 + scripts/bootstrap | 19 + scripts/format | 8 + scripts/lint | 8 + scripts/mock | 41 + scripts/test | 57 + scripts/utils/ruffen-docs.py | 167 ++ src/writer_ai/__init__.py | 93 + src/writer_ai/_base_client.py | 1991 +++++++++++++++++ src/writer_ai/_client.py | 426 ++++ src/writer_ai/_compat.py | 222 ++ src/writer_ai/_constants.py | 14 + src/writer_ai/_exceptions.py | 108 + src/writer_ai/_files.py | 127 ++ src/writer_ai/_models.py | 727 ++++++ src/writer_ai/_qs.py | 150 ++ src/writer_ai/_resource.py | 43 + src/writer_ai/_response.py | 820 +++++++ src/writer_ai/_streaming.py | 365 +++ src/writer_ai/_types.py | 220 ++ src/writer_ai/_utils/__init__.py | 51 + src/writer_ai/_utils/_logs.py | 25 + src/writer_ai/_utils/_proxy.py | 63 + src/writer_ai/_utils/_streams.py | 12 + src/writer_ai/_utils/_sync.py | 64 + src/writer_ai/_utils/_transform.py | 382 ++++ src/writer_ai/_utils/_typing.py | 120 + src/writer_ai/_utils/_utils.py | 403 ++++ src/writer_ai/_version.py | 4 + src/writer_ai/lib/.keep | 4 + src/writer_ai/py.typed | 0 src/writer_ai/resources/__init__.py | 47 + src/writer_ai/resources/chat.py | 182 ++ src/writer_ai/resources/completions.py | 380 ++++ src/writer_ai/resources/models.py | 115 + src/writer_ai/types/__init__.py | 10 + src/writer_ai/types/chat_chat_params.py | 32 + src/writer_ai/types/chat_chat_response.py | 30 + src/writer_ai/types/completion.py | 33 + .../types/completion_create_params.py | 37 + src/writer_ai/types/model_list_response.py | 17 + src/writer_ai/types/streaming_data.py | 11 + tests/__init__.py | 1 + tests/api_resources/__init__.py | 1 + tests/api_resources/test_chat.py | 158 ++ tests/api_resources/test_completions.py | 222 ++ tests/api_resources/test_models.py | 72 + tests/conftest.py | 49 + tests/sample_file.txt | 1 + tests/test_client.py | 1497 +++++++++++++ tests/test_deepcopy.py | 59 + tests/test_extract_files.py | 64 + tests/test_files.py | 51 + tests/test_models.py | 829 +++++++ tests/test_qs.py | 78 + tests/test_required_args.py | 111 + tests/test_response.py | 194 ++ tests/test_streaming.py | 248 ++ tests/test_transform.py | 408 ++++ tests/test_utils/test_proxy.py | 23 + tests/test_utils/test_typing.py | 78 + tests/utils.py | 151 ++ 79 files changed, 13207 insertions(+), 1 deletion(-) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .python-version create mode 100644 .stats.yml create mode 100644 Brewfile create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 api.md create mode 100644 bin/publish-pypi create mode 100644 examples/.keep create mode 100644 mypy.ini create mode 100644 noxfile.py create mode 100644 pyproject.toml create mode 100644 requirements-dev.lock create mode 100644 requirements.lock create mode 100755 scripts/bootstrap create mode 100755 scripts/format create mode 100755 scripts/lint create mode 100755 scripts/mock create mode 100755 scripts/test create mode 100644 scripts/utils/ruffen-docs.py create mode 100644 src/writer_ai/__init__.py create mode 100644 src/writer_ai/_base_client.py create mode 100644 src/writer_ai/_client.py create mode 100644 src/writer_ai/_compat.py create mode 100644 src/writer_ai/_constants.py create mode 100644 src/writer_ai/_exceptions.py create mode 100644 src/writer_ai/_files.py create mode 100644 src/writer_ai/_models.py create mode 100644 src/writer_ai/_qs.py create mode 100644 src/writer_ai/_resource.py create mode 100644 src/writer_ai/_response.py create mode 100644 src/writer_ai/_streaming.py create mode 100644 src/writer_ai/_types.py create mode 100644 src/writer_ai/_utils/__init__.py create mode 100644 src/writer_ai/_utils/_logs.py create mode 100644 src/writer_ai/_utils/_proxy.py create mode 100644 src/writer_ai/_utils/_streams.py create mode 100644 src/writer_ai/_utils/_sync.py create mode 100644 src/writer_ai/_utils/_transform.py create mode 100644 src/writer_ai/_utils/_typing.py create mode 100644 src/writer_ai/_utils/_utils.py create mode 100644 src/writer_ai/_version.py create mode 100644 src/writer_ai/lib/.keep create mode 100644 src/writer_ai/py.typed create mode 100644 src/writer_ai/resources/__init__.py create mode 100644 src/writer_ai/resources/chat.py create mode 100644 src/writer_ai/resources/completions.py create mode 100644 src/writer_ai/resources/models.py create mode 100644 src/writer_ai/types/__init__.py create mode 100644 src/writer_ai/types/chat_chat_params.py create mode 100644 src/writer_ai/types/chat_chat_response.py create mode 100644 src/writer_ai/types/completion.py create mode 100644 src/writer_ai/types/completion_create_params.py create mode 100644 src/writer_ai/types/model_list_response.py create mode 100644 src/writer_ai/types/streaming_data.py create mode 100644 tests/__init__.py create mode 100644 tests/api_resources/__init__.py create mode 100644 tests/api_resources/test_chat.py create mode 100644 tests/api_resources/test_completions.py create mode 100644 tests/api_resources/test_models.py create mode 100644 tests/conftest.py create mode 100644 tests/sample_file.txt create mode 100644 tests/test_client.py create mode 100644 tests/test_deepcopy.py create mode 100644 tests/test_extract_files.py create mode 100644 tests/test_files.py create mode 100644 tests/test_models.py create mode 100644 tests/test_qs.py create mode 100644 tests/test_required_args.py create mode 100644 tests/test_response.py create mode 100644 tests/test_streaming.py create mode 100644 tests/test_transform.py create mode 100644 tests/test_utils/test_proxy.py create mode 100644 tests/test_utils/test_typing.py create mode 100644 tests/utils.py diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..dd939620 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,9 @@ +ARG VARIANT="3.9" +FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} + +USER vscode + +RUN curl -sSf https://rye-up.com/get | RYE_VERSION="0.24.0" RYE_INSTALL_OPTION="--yes" bash +ENV PATH=/home/vscode/.rye/shims:$PATH + +RUN echo "[[ -d .venv ]] && source .venv/bin/activate" >> /home/vscode/.bashrc diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..bbeb30b1 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,40 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/debian +{ + "name": "Debian", + "build": { + "dockerfile": "Dockerfile", + "context": ".." + }, + + "postStartCommand": "rye sync --all-features", + + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python" + ], + "settings": { + "terminal.integrated.shell.linux": "/bin/bash", + "python.pythonPath": ".venv/bin/python", + "python.defaultInterpreterPath": ".venv/bin/python", + "python.typeChecking": "basic", + "terminal.integrated.env.linux": { + "PATH": "/home/vscode/.rye/shims:${env:PATH}" + } + } + } + } + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..c9b189a7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,62 @@ +name: CI +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + lint: + name: lint + runs-on: ubuntu-latest + + + steps: + - uses: actions/checkout@v4 + + - name: Install Rye + run: | + curl -sSf https://rye-up.com/get | bash + echo "$HOME/.rye/shims" >> $GITHUB_PATH + env: + RYE_VERSION: 0.24.0 + RYE_INSTALL_OPTION: '--yes' + + - name: Install dependencies + run: | + rye sync --all-features + + - name: Run ruff + run: | + rye run check:ruff + + - name: Run type checking + run: | + rye run typecheck + + - name: Ensure importable + run: | + rye run python -c 'import writer_ai' + test: + name: test + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install Rye + run: | + curl -sSf https://rye-up.com/get | bash + echo "$HOME/.rye/shims" >> $GITHUB_PATH + env: + RYE_VERSION: 0.24.0 + RYE_INSTALL_OPTION: '--yes' + + - name: Bootstrap + run: ./scripts/bootstrap + + - name: Run tests + run: ./scripts/test + diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..0f9a66a9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +.vscode +_dev + +__pycache__ +.mypy_cache + +dist + +.venv +.idea + +.env +.envrc +codegen.log +Brewfile.lock.json diff --git a/.python-version b/.python-version new file mode 100644 index 00000000..43077b24 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.9.18 diff --git a/.stats.yml b/.stats.yml new file mode 100644 index 00000000..9f22043a --- /dev/null +++ b/.stats.yml @@ -0,0 +1,2 @@ +configured_endpoints: 3 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-4a69d32ec3b55feece6cbe0daafff6eb6128e824e162a53ec615ff2ebbf750b6.yml diff --git a/Brewfile b/Brewfile new file mode 100644 index 00000000..492ca37b --- /dev/null +++ b/Brewfile @@ -0,0 +1,2 @@ +brew "rye" + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..7a6dbff0 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,125 @@ +## Setting up the environment + +### With Rye + +We use [Rye](https://rye-up.com/) to manage dependencies so we highly recommend [installing it](https://rye-up.com/guide/installation/) as it will automatically provision a Python environment with the expected Python version. + +After installing Rye, you'll just have to run this command: + +```sh +$ rye sync --all-features +``` + +You can then run scripts using `rye run python script.py` or by activating the virtual environment: + +```sh +$ rye shell +# or manually activate - https://docs.python.org/3/library/venv.html#how-venvs-work +$ source .venv/bin/activate + +# now you can omit the `rye run` prefix +$ python script.py +``` + +### Without Rye + +Alternatively if you don't want to install `Rye`, you can stick with the standard `pip` setup by ensuring you have the Python version specified in `.python-version`, create a virtual environment however you desire and then install dependencies using this command: + +```sh +$ pip install -r requirements-dev.lock +``` + +## Modifying/Adding code + +Most of the SDK is generated code, and any modified code will be overridden on the next generation. The +`src/writer_ai/lib/` and `examples/` directories are exceptions and will never be overridden. + +## Adding and running examples + +All files in the `examples/` directory are not modified by the Stainless generator and can be freely edited or +added to. + +```bash +# add an example to examples/.py + +#!/usr/bin/env -S rye run python +… +``` + +``` +chmod +x examples/.py +# run the example against your api +./examples/.py +``` + +## Using the repository from source + +If you’d like to use the repository from source, you can either install from git or link to a cloned repository: + +To install via git: + +```bash +pip install git+ssh://git@github.com/stainless-sdks/writerai/writer-python.git +``` + +Alternatively, you can build from source and install the wheel file: + +Building this package will create two files in the `dist/` directory, a `.tar.gz` containing the source files and a `.whl` that can be used to install the package efficiently. + +To create a distributable version of the library, all you have to do is run this command: + +```bash +rye build +# or +python -m build +``` + +Then to install: + +```sh +pip install ./path-to-wheel-file.whl +``` + +## Running tests + +Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests. + +```bash +# you will need npm installed +npx prism mock path/to/your/openapi.yml +``` + +```bash +rye run pytest +``` + +## Linting and formatting + +This repository uses [ruff](https://github.com/astral-sh/ruff) and +[black](https://github.com/psf/black) to format the code in the repository. + +To lint: + +```bash +rye run lint +``` + +To format and fix all ruff issues automatically: + +```bash +rye run format +``` + +## Publishing and releases + +Changes made to this repository via the automated release PR pipeline should publish to PyPI automatically. If +the changes aren't made through the automated pipeline, you may want to make releases manually. + +### Publish with a GitHub workflow + +You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/stainless-sdks/writerai/writer-python/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. + +### Publish manually + +If you need to manually release a package, you can run the `bin/publish-pypi` script with a `PYPI_TOKEN` set on +the environment. diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..38aac4e7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2024 Writer AI + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 9c18f8ee..a1314426 100644 --- a/README.md +++ b/README.md @@ -1 +1,391 @@ -# writer-python \ No newline at end of file +# Writer AI Python API library + +[![PyPI version](https://img.shields.io/pypi/v/writerai.svg)](https://pypi.org/project/writerai/) + +The Writer AI Python library provides convenient access to the Writer AI REST API from any Python 3.7+ +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.stainlessapi.com/). + +## Documentation + +The REST API documentation can be found [on dev.writer.com](https://dev.writer.com/docs/quickstart). The full API of this library can be found in [api.md](api.md). + +## Installation + +```sh +# install from this staging repo +pip install git+ssh://git@github.com/stainless-sdks/writerai/writer-python.git +``` + +> [!NOTE] +> Once this package is [published to PyPI](https://app.stainlessapi.com/docs/guides/publish), this will become: `pip install --pre writerai` + +## Usage + +The full API of this library can be found in [api.md](api.md). + +```python +import os +from writer_ai import WriterAI + +client = WriterAI( + # This is the default and can be omitted + api_key=os.environ.get("WRITERAI_AUTH_TOKEN"), +) + +chat_chat_response = client.chat.chat( + messages=[ + { + "content": "string", + "role": "user", + } + ], + model="string", +) +print(chat_chat_response.id) +``` + +While you can provide an `api_key` keyword argument, +we recommend using [python-dotenv](https://pypi.org/project/python-dotenv/) +to add `WRITERAI_AUTH_TOKEN="My API Key"` to your `.env` file +so that your API Key is not stored in source control. + +## Async usage + +Simply import `AsyncWriterAI` instead of `WriterAI` and use `await` with each API call: + +```python +import os +import asyncio +from writer_ai import AsyncWriterAI + +client = AsyncWriterAI( + # This is the default and can be omitted + api_key=os.environ.get("WRITERAI_AUTH_TOKEN"), +) + + +async def main() -> None: + chat_chat_response = await client.chat.chat( + messages=[ + { + "content": "string", + "role": "user", + } + ], + model="string", + ) + print(chat_chat_response.id) + + +asyncio.run(main()) +``` + +Functionality between the synchronous and asynchronous clients is otherwise identical. + +## Streaming responses + +We provide support for streaming responses using Server Side Events (SSE). + +```python +from writer_ai import WriterAI + +client = WriterAI() + +stream = client.completions.create( + model="palmyra-x-v2", + prompt="Hi, my name is", + stream=True, +) +for completion in stream: + print(completion.choices) +``` + +The async client uses the exact same interface. + +```python +from writer_ai import AsyncWriterAI + +client = AsyncWriterAI() + +stream = await client.completions.create( + model="palmyra-x-v2", + prompt="Hi, my name is", + stream=True, +) +async for completion in stream: + print(completion.choices) +``` + +## Using types + +Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev) which also provide helper methods for things like: + +- Serializing back into JSON, `model.to_json()` +- Converting to a dictionary, `model.to_dict()` + +Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`. + +## Handling errors + +When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `writer_ai.APIConnectionError` is raised. + +When the API returns a non-success status code (that is, 4xx or 5xx +response), a subclass of `writer_ai.APIStatusError` is raised, containing `status_code` and `response` properties. + +All errors inherit from `writer_ai.APIError`. + +```python +import writer_ai +from writer_ai import WriterAI + +client = WriterAI() + +try: + client.chat.chat( + messages=[ + { + "content": "string", + "role": "user", + } + ], + model="string", + ) +except writer_ai.APIConnectionError as e: + print("The server could not be reached") + print(e.__cause__) # an underlying Exception, likely raised within httpx. +except writer_ai.RateLimitError as e: + print("A 429 status code was received; we should back off a bit.") +except writer_ai.APIStatusError as e: + print("Another non-200-range status code was received") + print(e.status_code) + print(e.response) +``` + +Error codes are as followed: + +| Status Code | Error Type | +| ----------- | -------------------------- | +| 400 | `BadRequestError` | +| 401 | `AuthenticationError` | +| 403 | `PermissionDeniedError` | +| 404 | `NotFoundError` | +| 422 | `UnprocessableEntityError` | +| 429 | `RateLimitError` | +| >=500 | `InternalServerError` | +| N/A | `APIConnectionError` | + +### Retries + +Certain errors are automatically retried 2 times by default, with a short exponential backoff. +Connection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict, +429 Rate Limit, and >=500 Internal errors are all retried by default. + +You can use the `max_retries` option to configure or disable retry settings: + +```python +from writer_ai import WriterAI + +# Configure the default for all requests: +client = WriterAI( + # default is 2 + max_retries=0, +) + +# Or, configure per-request: +client.with_options(max_retries=5).chat.chat( + messages=[ + { + "content": "string", + "role": "user", + } + ], + model="string", +) +``` + +### Timeouts + +By default requests time out after 1 minute. You can configure this with a `timeout` option, +which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/#fine-tuning-the-configuration) object: + +```python +from writer_ai import WriterAI + +# Configure the default for all requests: +client = WriterAI( + # 20 seconds (default is 1 minute) + timeout=20.0, +) + +# More granular control: +client = WriterAI( + timeout=httpx.Timeout(60.0, read=5.0, write=10.0, connect=2.0), +) + +# Override per-request: +client.with_options(timeout=5 * 1000).chat.chat( + messages=[ + { + "content": "string", + "role": "user", + } + ], + model="string", +) +``` + +On timeout, an `APITimeoutError` is thrown. + +Note that requests that time out are [retried twice by default](#retries). + +## Advanced + +### Logging + +We use the standard library [`logging`](https://docs.python.org/3/library/logging.html) module. + +You can enable logging by setting the environment variable `WRITER_AI_LOG` to `debug`. + +```shell +$ export WRITER_AI_LOG=debug +``` + +### How to tell whether `None` means `null` or missing + +In an API response, a field may be explicitly `null`, or missing entirely; in either case, its value is `None` in this library. You can differentiate the two cases with `.model_fields_set`: + +```py +if response.my_field is None: + if 'my_field' not in response.model_fields_set: + print('Got json like {}, without a "my_field" key present at all.') + else: + print('Got json like {"my_field": null}.') +``` + +### Accessing raw response data (e.g. headers) + +The "raw" Response object can be accessed by prefixing `.with_raw_response.` to any HTTP method call, e.g., + +```py +from writer_ai import WriterAI + +client = WriterAI() +response = client.chat.with_raw_response.chat( + messages=[{ + "content": "string", + "role": "user", + }], + model="string", +) +print(response.headers.get('X-My-Header')) + +chat = response.parse() # get the object that `chat.chat()` would have returned +print(chat.id) +``` + +These methods return an [`APIResponse`](https://github.com/stainless-sdks/tree/main/src/writer_ai/_response.py) object. + +The async client returns an [`AsyncAPIResponse`](https://github.com/stainless-sdks/tree/main/src/writer_ai/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. + +#### `.with_streaming_response` + +The above interface eagerly reads the full response body when you make the request, which may not always be what you want. + +To stream the response body, use `.with_streaming_response` instead, which requires a context manager and only reads the response body once you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()` or `.parse()`. In the async client, these are async methods. + +```python +with client.chat.with_streaming_response.chat( + messages=[ + { + "content": "string", + "role": "user", + } + ], + model="string", +) as response: + print(response.headers.get("X-My-Header")) + + for line in response.iter_lines(): + print(line) +``` + +The context manager is required so that the response will reliably be closed. + +### Making custom/undocumented requests + +This library is typed for convenient access to the documented API. + +If you need to access undocumented endpoints, params, or response properties, the library can still be used. + +#### Undocumented endpoints + +To make requests to undocumented endpoints, you can make requests using `client.get`, `client.post`, and other +http verbs. Options on the client will be respected (such as retries) will be respected when making this +request. + +```py +import httpx + +response = client.post( + "/foo", + cast_to=httpx.Response, + body={"my_param": True}, +) + +print(response.headers.get("x-foo")) +``` + +#### Undocumented request params + +If you want to explicitly send an extra param, you can do so with the `extra_query`, `extra_body`, and `extra_headers` request +options. + +#### Undocumented response properties + +To access undocumented response properties, you can access the extra fields like `response.unknown_prop`. You +can also get all the extra fields on the Pydantic model as a dict with +[`response.model_extra`](https://docs.pydantic.dev/latest/api/base_model/#pydantic.BaseModel.model_extra). + +### Configuring the HTTP client + +You can directly override the [httpx client](https://www.python-httpx.org/api/#client) to customize it for your use case, including: + +- Support for proxies +- Custom transports +- Additional [advanced](https://www.python-httpx.org/advanced/#client-instances) functionality + +```python +from writer_ai import WriterAI, DefaultHttpxClient + +client = WriterAI( + # Or use the `WRITER_AI_BASE_URL` env var + base_url="http://my.test.server.example.com:8083", + http_client=DefaultHttpxClient( + proxies="http://my.test.proxy.example.com", + transport=httpx.HTTPTransport(local_address="0.0.0.0"), + ), +) +``` + +### Managing HTTP resources + +By default the library closes underlying HTTP connections whenever the client is [garbage collected](https://docs.python.org/3/reference/datamodel.html#object.__del__). You can manually close the client using the `.close()` method if desired, or with a context manager that closes when exiting. + +## Versioning + +This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions: + +1. Changes that only affect static types, without breaking runtime behavior. +2. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals)_. +3. Changes that we do not expect to impact the vast majority of users in practice. + +We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. + +We are keen for your feedback; please open an [issue](https://www.github.com/stainless-sdks/writerai/writer-python/issues) with questions, bugs, or suggestions. + +## Requirements + +Python 3.7 or higher. diff --git a/api.md b/api.md new file mode 100644 index 00000000..d5378453 --- /dev/null +++ b/api.md @@ -0,0 +1,35 @@ +# Chat + +Types: + +```python +from writer_ai.types import ChatChatResponse +``` + +Methods: + +- client.chat.chat(\*\*params) -> ChatChatResponse + +# Completions + +Types: + +```python +from writer_ai.types import Completion, StreamingData +``` + +Methods: + +- client.completions.create(\*\*params) -> Completion + +# Models + +Types: + +```python +from writer_ai.types import ModelListResponse +``` + +Methods: + +- client.models.list() -> ModelListResponse diff --git a/bin/publish-pypi b/bin/publish-pypi new file mode 100644 index 00000000..826054e9 --- /dev/null +++ b/bin/publish-pypi @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -eux +mkdir -p dist +rye build --clean +rye publish --yes --token=$PYPI_TOKEN diff --git a/examples/.keep b/examples/.keep new file mode 100644 index 00000000..d8c73e93 --- /dev/null +++ b/examples/.keep @@ -0,0 +1,4 @@ +File generated from our OpenAPI spec by Stainless. + +This directory can be used to store example files demonstrating usage of this SDK. +It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. \ No newline at end of file diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..5da83123 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,47 @@ +[mypy] +pretty = True +show_error_codes = True + +# Exclude _files.py because mypy isn't smart enough to apply +# the correct type narrowing and as this is an internal module +# it's fine to just use Pyright. +exclude = ^(src/writer_ai/_files\.py|_dev/.*\.py)$ + +strict_equality = True +implicit_reexport = True +check_untyped_defs = True +no_implicit_optional = True + +warn_return_any = True +warn_unreachable = True +warn_unused_configs = True + +# Turn these options off as it could cause conflicts +# with the Pyright options. +warn_unused_ignores = False +warn_redundant_casts = False + +disallow_any_generics = True +disallow_untyped_defs = True +disallow_untyped_calls = True +disallow_subclassing_any = True +disallow_incomplete_defs = True +disallow_untyped_decorators = True +cache_fine_grained = True + +# By default, mypy reports an error if you assign a value to the result +# of a function call that doesn't return anything. We do this in our test +# cases: +# ``` +# result = ... +# assert result is None +# ``` +# Changing this codegen to make mypy happy would increase complexity +# and would not be worth it. +disable_error_code = func-returns-value + +# https://github.com/python/mypy/issues/12162 +[mypy.overrides] +module = "black.files.*" +ignore_errors = true +ignore_missing_imports = true diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 00000000..53bca7ff --- /dev/null +++ b/noxfile.py @@ -0,0 +1,9 @@ +import nox + + +@nox.session(reuse_venv=True, name="test-pydantic-v1") +def test_pydantic_v1(session: nox.Session) -> None: + session.install("-r", "requirements-dev.lock") + session.install("pydantic<2") + + session.run("pytest", "--showlocals", "--ignore=tests/functional", *session.posargs) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..eb651b7b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,196 @@ +[project] +name = "writerai" +version = "0.0.1-alpha.0" +description = "The official Python library for the writerai API" +dynamic = ["readme"] +license = "Apache-2.0" +authors = [ +{ name = "Writer AI", email = "dev-feedback@writer.com" }, +] +dependencies = [ + "httpx>=0.23.0, <1", + "pydantic>=1.9.0, <3", + "typing-extensions>=4.7, <5", + "anyio>=3.5.0, <5", + "distro>=1.7.0, <2", + "sniffio", + "cached-property; python_version < '3.8'", + +] +requires-python = ">= 3.7" +classifiers = [ + "Typing :: Typed", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Operating System :: OS Independent", + "Operating System :: POSIX", + "Operating System :: MacOS", + "Operating System :: POSIX :: Linux", + "Operating System :: Microsoft :: Windows", + "Topic :: Software Development :: Libraries :: Python Modules", + "License :: OSI Approved :: Apache Software License" +] + + + +[project.urls] +Homepage = "https://github.com/stainless-sdks/writerai/writer-python" +Repository = "https://github.com/stainless-sdks/writerai/writer-python" + + + +[tool.rye] +managed = true +# version pins are in requirements-dev.lock +dev-dependencies = [ + "pyright>=1.1.359", + "mypy", + "respx", + "pytest", + "pytest-asyncio", + "ruff", + "time-machine", + "nox", + "dirty-equals>=0.6.0", + "importlib-metadata>=6.7.0", + +] + +[tool.rye.scripts] +format = { chain = [ + "format:ruff", + "format:docs", + "fix:ruff", +]} +"format:black" = "black ." +"format:docs" = "python scripts/utils/ruffen-docs.py README.md api.md" +"format:ruff" = "ruff format" +"format:isort" = "isort ." + +"lint" = { chain = [ + "check:ruff", + "typecheck", +]} +"check:ruff" = "ruff ." +"fix:ruff" = "ruff --fix ." + +typecheck = { chain = [ + "typecheck:pyright", + "typecheck:mypy" +]} +"typecheck:pyright" = "pyright" +"typecheck:verify-types" = "pyright --verifytypes writer_ai --ignoreexternal" +"typecheck:mypy" = "mypy ." + +[build-system] +requires = ["hatchling", "hatch-fancy-pypi-readme"] +build-backend = "hatchling.build" + +[tool.hatch.build] +include = [ + "src/*" +] + +[tool.hatch.build.targets.wheel] +packages = ["src/writer_ai"] + +[tool.hatch.metadata.hooks.fancy-pypi-readme] +content-type = "text/markdown" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] +path = "README.md" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]] +# replace relative links with absolute links +pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)' +replacement = '[\1](https://github.com/stainless-sdks/tree/main/\g<2>)' + +[tool.black] +line-length = 120 +target-version = ["py37"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = "--tb=short" +xfail_strict = true +asyncio_mode = "auto" +filterwarnings = [ + "error" +] + +[tool.pyright] +# this enables practically every flag given by pyright. +# there are a couple of flags that are still disabled by +# default in strict mode as they are experimental and niche. +typeCheckingMode = "strict" +pythonVersion = "3.7" + +exclude = [ + "_dev", + ".venv", + ".nox", +] + +reportImplicitOverride = true + +reportImportCycles = false +reportPrivateUsage = false + + +[tool.ruff] +line-length = 120 +output-format = "grouped" +target-version = "py37" +select = [ + # isort + "I", + # bugbear rules + "B", + # remove unused imports + "F401", + # bare except statements + "E722", + # unused arguments + "ARG", + # print statements + "T201", + "T203", + # misuse of typing.TYPE_CHECKING + "TCH004", + # import rules + "TID251", +] +ignore = [ + # mutable defaults + "B006", +] +unfixable = [ + # disable auto fix for print statements + "T201", + "T203", +] +ignore-init-module-imports = true + +[tool.ruff.format] +docstring-code-format = true + +[tool.ruff.lint.flake8-tidy-imports.banned-api] +"functools.lru_cache".msg = "This function does not retain type information for the wrapped function's arguments; The `lru_cache` function from `_utils` should be used instead" + +[tool.ruff.lint.isort] +length-sort = true +length-sort-straight = true +combine-as-imports = true +extra-standard-library = ["typing_extensions"] +known-first-party = ["writer_ai", "tests"] + +[tool.ruff.per-file-ignores] +"bin/**.py" = ["T201", "T203"] +"scripts/**.py" = ["T201", "T203"] +"tests/**.py" = ["T201", "T203"] +"examples/**.py" = ["T201", "T203"] diff --git a/requirements-dev.lock b/requirements-dev.lock new file mode 100644 index 00000000..02f94023 --- /dev/null +++ b/requirements-dev.lock @@ -0,0 +1,96 @@ +# generated by rye +# use `rye lock` or `rye sync` to update this lockfile +# +# last locked with the following flags: +# pre: false +# features: [] +# all-features: true +# with-sources: false + +-e file:. +annotated-types==0.6.0 + # via pydantic +anyio==4.1.0 + # via httpx + # via writerai +argcomplete==3.1.2 + # via nox +attrs==23.1.0 + # via pytest +certifi==2023.7.22 + # via httpcore + # via httpx +colorlog==6.7.0 + # via nox +dirty-equals==0.6.0 +distlib==0.3.7 + # via virtualenv +distro==1.8.0 + # via writerai +exceptiongroup==1.1.3 + # via anyio +filelock==3.12.4 + # via virtualenv +h11==0.14.0 + # via httpcore +httpcore==1.0.2 + # via httpx +httpx==0.25.2 + # via respx + # via writerai +idna==3.4 + # via anyio + # via httpx +importlib-metadata==7.0.0 +iniconfig==2.0.0 + # via pytest +mypy==1.7.1 +mypy-extensions==1.0.0 + # via mypy +nodeenv==1.8.0 + # via pyright +nox==2023.4.22 +packaging==23.2 + # via nox + # via pytest +platformdirs==3.11.0 + # via virtualenv +pluggy==1.3.0 + # via pytest +py==1.11.0 + # via pytest +pydantic==2.4.2 + # via writerai +pydantic-core==2.10.1 + # via pydantic +pyright==1.1.359 +pytest==7.1.1 + # via pytest-asyncio +pytest-asyncio==0.21.1 +python-dateutil==2.8.2 + # via time-machine +pytz==2023.3.post1 + # via dirty-equals +respx==0.20.2 +ruff==0.1.9 +setuptools==68.2.2 + # via nodeenv +six==1.16.0 + # via python-dateutil +sniffio==1.3.0 + # via anyio + # via httpx + # via writerai +time-machine==2.9.0 +tomli==2.0.1 + # via mypy + # via pytest +typing-extensions==4.8.0 + # via mypy + # via pydantic + # via pydantic-core + # via writerai +virtualenv==20.24.5 + # via nox +zipp==3.17.0 + # via importlib-metadata diff --git a/requirements.lock b/requirements.lock new file mode 100644 index 00000000..81d35ad8 --- /dev/null +++ b/requirements.lock @@ -0,0 +1,43 @@ +# generated by rye +# use `rye lock` or `rye sync` to update this lockfile +# +# last locked with the following flags: +# pre: false +# features: [] +# all-features: true +# with-sources: false + +-e file:. +annotated-types==0.6.0 + # via pydantic +anyio==4.1.0 + # via httpx + # via writerai +certifi==2023.7.22 + # via httpcore + # via httpx +distro==1.8.0 + # via writerai +exceptiongroup==1.1.3 + # via anyio +h11==0.14.0 + # via httpcore +httpcore==1.0.2 + # via httpx +httpx==0.25.2 + # via writerai +idna==3.4 + # via anyio + # via httpx +pydantic==2.4.2 + # via writerai +pydantic-core==2.10.1 + # via pydantic +sniffio==1.3.0 + # via anyio + # via httpx + # via writerai +typing-extensions==4.8.0 + # via pydantic + # via pydantic-core + # via writerai diff --git a/scripts/bootstrap b/scripts/bootstrap new file mode 100755 index 00000000..29df07e7 --- /dev/null +++ b/scripts/bootstrap @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ]; then + brew bundle check >/dev/null 2>&1 || { + echo "==> Installing Homebrew dependencies…" + brew bundle + } +fi + +echo "==> Installing Python dependencies…" + +# experimental uv support makes installations significantly faster +rye config --set-bool behavior.use-uv=true + +rye sync diff --git a/scripts/format b/scripts/format new file mode 100755 index 00000000..2a9ea466 --- /dev/null +++ b/scripts/format @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +rye run format + diff --git a/scripts/lint b/scripts/lint new file mode 100755 index 00000000..0cc68b51 --- /dev/null +++ b/scripts/lint @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +rye run lint + diff --git a/scripts/mock b/scripts/mock new file mode 100755 index 00000000..fe89a1d0 --- /dev/null +++ b/scripts/mock @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +if [[ -n "$1" && "$1" != '--'* ]]; then + URL="$1" + shift +else + URL="$(grep 'openapi_spec_url' .stats.yml | cut -d' ' -f2)" +fi + +# Check if the URL is empty +if [ -z "$URL" ]; then + echo "Error: No OpenAPI spec path/url provided or found in .stats.yml" + exit 1 +fi + +echo "==> Starting mock server with URL ${URL}" + +# Run prism mock on the given spec +if [ "$1" == "--daemon" ]; then + npm exec --package=@stoplight/prism-cli@~5.8 -- 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=@stoplight/prism-cli@~5.8 -- prism mock "$URL" +fi diff --git a/scripts/test b/scripts/test new file mode 100755 index 00000000..be01d044 --- /dev/null +++ b/scripts/test @@ -0,0 +1,57 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +NC='\033[0m' # No Color + +function prism_is_running() { + curl --silent "http://localhost:4010" >/dev/null 2>&1 +} + +kill_server_on_port() { + pids=$(lsof -t -i tcp:"$1" || echo "") + if [ "$pids" != "" ]; then + kill "$pids" + echo "Stopped $pids." + fi +} + +function is_overriding_api_base_url() { + [ -n "$TEST_API_BASE_URL" ] +} + +if ! is_overriding_api_base_url && ! prism_is_running ; then + # When we exit this script, make sure to kill the background mock server process + trap 'kill_server_on_port 4010' EXIT + + # Start the dev server + ./scripts/mock --daemon +fi + +if is_overriding_api_base_url ; then + echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}" + echo +elif ! prism_is_running ; then + echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server" + echo -e "running against your OpenAPI spec." + echo + echo -e "To run the server, pass in the path or url of your OpenAPI" + echo -e "spec to the prism command:" + echo + echo -e " \$ ${YELLOW}npm exec --package=@stoplight/prism-cli@~5.3.2 -- prism mock path/to/your.openapi.yml${NC}" + echo + + exit 1 +else + echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}" + echo +fi + +# Run tests +echo "==> Running tests" +rye run pytest "$@" diff --git a/scripts/utils/ruffen-docs.py b/scripts/utils/ruffen-docs.py new file mode 100644 index 00000000..37b3d94f --- /dev/null +++ b/scripts/utils/ruffen-docs.py @@ -0,0 +1,167 @@ +# fork of https://github.com/asottile/blacken-docs adapted for ruff +from __future__ import annotations + +import re +import sys +import argparse +import textwrap +import contextlib +import subprocess +from typing import Match, Optional, Sequence, Generator, NamedTuple, cast + +MD_RE = re.compile( + r"(?P^(?P *)```\s*python\n)" r"(?P.*?)" r"(?P^(?P=indent)```\s*$)", + re.DOTALL | re.MULTILINE, +) +MD_PYCON_RE = re.compile( + r"(?P^(?P *)```\s*pycon\n)" r"(?P.*?)" r"(?P^(?P=indent)```.*$)", + re.DOTALL | re.MULTILINE, +) +PYCON_PREFIX = ">>> " +PYCON_CONTINUATION_PREFIX = "..." +PYCON_CONTINUATION_RE = re.compile( + rf"^{re.escape(PYCON_CONTINUATION_PREFIX)}( |$)", +) +DEFAULT_LINE_LENGTH = 100 + + +class CodeBlockError(NamedTuple): + offset: int + exc: Exception + + +def format_str( + src: str, +) -> tuple[str, Sequence[CodeBlockError]]: + errors: list[CodeBlockError] = [] + + @contextlib.contextmanager + def _collect_error(match: Match[str]) -> Generator[None, None, None]: + try: + yield + except Exception as e: + errors.append(CodeBlockError(match.start(), e)) + + def _md_match(match: Match[str]) -> str: + code = textwrap.dedent(match["code"]) + with _collect_error(match): + code = format_code_block(code) + code = textwrap.indent(code, match["indent"]) + return f'{match["before"]}{code}{match["after"]}' + + def _pycon_match(match: Match[str]) -> str: + code = "" + fragment = cast(Optional[str], None) + + def finish_fragment() -> None: + nonlocal code + nonlocal fragment + + if fragment is not None: + with _collect_error(match): + fragment = format_code_block(fragment) + fragment_lines = fragment.splitlines() + code += f"{PYCON_PREFIX}{fragment_lines[0]}\n" + for line in fragment_lines[1:]: + # Skip blank lines to handle Black adding a blank above + # functions within blocks. A blank line would end the REPL + # continuation prompt. + # + # >>> if True: + # ... def f(): + # ... pass + # ... + if line: + code += f"{PYCON_CONTINUATION_PREFIX} {line}\n" + if fragment_lines[-1].startswith(" "): + code += f"{PYCON_CONTINUATION_PREFIX}\n" + fragment = None + + indentation = None + for line in match["code"].splitlines(): + orig_line, line = line, line.lstrip() + if indentation is None and line: + indentation = len(orig_line) - len(line) + continuation_match = PYCON_CONTINUATION_RE.match(line) + if continuation_match and fragment is not None: + fragment += line[continuation_match.end() :] + "\n" + else: + finish_fragment() + if line.startswith(PYCON_PREFIX): + fragment = line[len(PYCON_PREFIX) :] + "\n" + else: + code += orig_line[indentation:] + "\n" + finish_fragment() + return code + + def _md_pycon_match(match: Match[str]) -> str: + code = _pycon_match(match) + code = textwrap.indent(code, match["indent"]) + return f'{match["before"]}{code}{match["after"]}' + + src = MD_RE.sub(_md_match, src) + src = MD_PYCON_RE.sub(_md_pycon_match, src) + return src, errors + + +def format_code_block(code: str) -> str: + return subprocess.check_output( + [ + sys.executable, + "-m", + "ruff", + "format", + "--stdin-filename=script.py", + f"--line-length={DEFAULT_LINE_LENGTH}", + ], + encoding="utf-8", + input=code, + ) + + +def format_file( + filename: str, + skip_errors: bool, +) -> int: + with open(filename, encoding="UTF-8") as f: + contents = f.read() + new_contents, errors = format_str(contents) + for error in errors: + lineno = contents[: error.offset].count("\n") + 1 + print(f"{filename}:{lineno}: code block parse error {error.exc}") + if errors and not skip_errors: + return 1 + if contents != new_contents: + print(f"{filename}: Rewriting...") + with open(filename, "w", encoding="UTF-8") as f: + f.write(new_contents) + return 0 + else: + return 0 + + +def main(argv: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser() + parser.add_argument( + "-l", + "--line-length", + type=int, + default=DEFAULT_LINE_LENGTH, + ) + parser.add_argument( + "-S", + "--skip-string-normalization", + action="store_true", + ) + parser.add_argument("-E", "--skip-errors", action="store_true") + parser.add_argument("filenames", nargs="*") + args = parser.parse_args(argv) + + retv = 0 + for filename in args.filenames: + retv |= format_file(filename, skip_errors=args.skip_errors) + return retv + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/writer_ai/__init__.py b/src/writer_ai/__init__.py new file mode 100644 index 00000000..8c76f13a --- /dev/null +++ b/src/writer_ai/__init__.py @@ -0,0 +1,93 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from . import types +from ._types import NOT_GIVEN, NoneType, NotGiven, Transport, ProxiesTypes +from ._utils import file_from_path +from ._client import ( + Client, + Stream, + Timeout, + WriterAI, + Transport, + AsyncClient, + AsyncStream, + AsyncWriterAI, + RequestOptions, +) +from ._models import BaseModel +from ._version import __title__, __version__ +from ._response import APIResponse as APIResponse, AsyncAPIResponse as AsyncAPIResponse +from ._constants import DEFAULT_TIMEOUT, DEFAULT_MAX_RETRIES, DEFAULT_CONNECTION_LIMITS +from ._exceptions import ( + APIError, + ConflictError, + NotFoundError, + WriterAIError, + APIStatusError, + RateLimitError, + APITimeoutError, + BadRequestError, + APIConnectionError, + AuthenticationError, + InternalServerError, + PermissionDeniedError, + UnprocessableEntityError, + APIResponseValidationError, +) +from ._base_client import DefaultHttpxClient, DefaultAsyncHttpxClient +from ._utils._logs import setup_logging as _setup_logging + +__all__ = [ + "types", + "__version__", + "__title__", + "NoneType", + "Transport", + "ProxiesTypes", + "NotGiven", + "NOT_GIVEN", + "WriterAIError", + "APIError", + "APIStatusError", + "APITimeoutError", + "APIConnectionError", + "APIResponseValidationError", + "BadRequestError", + "AuthenticationError", + "PermissionDeniedError", + "NotFoundError", + "ConflictError", + "UnprocessableEntityError", + "RateLimitError", + "InternalServerError", + "Timeout", + "RequestOptions", + "Client", + "AsyncClient", + "Stream", + "AsyncStream", + "WriterAI", + "AsyncWriterAI", + "file_from_path", + "BaseModel", + "DEFAULT_TIMEOUT", + "DEFAULT_MAX_RETRIES", + "DEFAULT_CONNECTION_LIMITS", + "DefaultHttpxClient", + "DefaultAsyncHttpxClient", +] + +_setup_logging() + +# Update the __module__ attribute for exported symbols so that +# error messages point to this module instead of the module +# it was originally defined in, e.g. +# writer_ai._exceptions.NotFoundError -> writer_ai.NotFoundError +__locals = locals() +for __name in __all__: + if not __name.startswith("__"): + try: + __locals[__name].__module__ = "writer_ai" + except (TypeError, AttributeError): + # Some of our exported symbols are builtins which we can't set attributes for. + pass diff --git a/src/writer_ai/_base_client.py b/src/writer_ai/_base_client.py new file mode 100644 index 00000000..f0896225 --- /dev/null +++ b/src/writer_ai/_base_client.py @@ -0,0 +1,1991 @@ +from __future__ import annotations + +import json +import time +import uuid +import email +import asyncio +import inspect +import logging +import platform +import warnings +import email.utils +from types import TracebackType +from random import random +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Type, + Union, + Generic, + Mapping, + TypeVar, + Iterable, + Iterator, + Optional, + Generator, + AsyncIterator, + cast, + overload, +) +from typing_extensions import Literal, override, get_origin + +import anyio +import httpx +import distro +import pydantic +from httpx import URL, Limits +from pydantic import PrivateAttr + +from . import _exceptions +from ._qs import Querystring +from ._files import to_httpx_files, async_to_httpx_files +from ._types import ( + NOT_GIVEN, + Body, + Omit, + Query, + Headers, + Timeout, + NotGiven, + ResponseT, + Transport, + AnyMapping, + PostParser, + ProxiesTypes, + RequestFiles, + HttpxSendArgs, + AsyncTransport, + RequestOptions, + ModelBuilderProtocol, +) +from ._utils import is_dict, is_list, is_given, lru_cache, is_mapping +from ._compat import model_copy, model_dump +from ._models import GenericModel, FinalRequestOptions, validate_type, construct_type +from ._response import ( + APIResponse, + BaseAPIResponse, + AsyncAPIResponse, + extract_response_type, +) +from ._constants import ( + DEFAULT_TIMEOUT, + MAX_RETRY_DELAY, + DEFAULT_MAX_RETRIES, + INITIAL_RETRY_DELAY, + RAW_RESPONSE_HEADER, + OVERRIDE_CAST_TO_HEADER, + DEFAULT_CONNECTION_LIMITS, +) +from ._streaming import Stream, SSEDecoder, AsyncStream, SSEBytesDecoder +from ._exceptions import ( + APIStatusError, + APITimeoutError, + APIConnectionError, + APIResponseValidationError, +) + +log: logging.Logger = logging.getLogger(__name__) + +# TODO: make base page type vars covariant +SyncPageT = TypeVar("SyncPageT", bound="BaseSyncPage[Any]") +AsyncPageT = TypeVar("AsyncPageT", bound="BaseAsyncPage[Any]") + + +_T = TypeVar("_T") +_T_co = TypeVar("_T_co", covariant=True) + +_StreamT = TypeVar("_StreamT", bound=Stream[Any]) +_AsyncStreamT = TypeVar("_AsyncStreamT", bound=AsyncStream[Any]) + +if TYPE_CHECKING: + from httpx._config import DEFAULT_TIMEOUT_CONFIG as HTTPX_DEFAULT_TIMEOUT +else: + try: + from httpx._config import DEFAULT_TIMEOUT_CONFIG as HTTPX_DEFAULT_TIMEOUT + except ImportError: + # taken from https://github.com/encode/httpx/blob/3ba5fe0d7ac70222590e759c31442b1cab263791/httpx/_config.py#L366 + HTTPX_DEFAULT_TIMEOUT = Timeout(5.0) + + +class PageInfo: + """Stores the necessary information to build the request to retrieve the next page. + + Either `url` or `params` must be set. + """ + + url: URL | NotGiven + params: Query | NotGiven + + @overload + def __init__( + self, + *, + url: URL, + ) -> None: + ... + + @overload + def __init__( + self, + *, + params: Query, + ) -> None: + ... + + def __init__( + self, + *, + url: URL | NotGiven = NOT_GIVEN, + params: Query | NotGiven = NOT_GIVEN, + ) -> None: + self.url = url + self.params = params + + +class BasePage(GenericModel, Generic[_T]): + """ + Defines the core interface for pagination. + + Type Args: + ModelT: The pydantic model that represents an item in the response. + + Methods: + has_next_page(): Check if there is another page available + next_page_info(): Get the necessary information to make a request for the next page + """ + + _options: FinalRequestOptions = PrivateAttr() + _model: Type[_T] = PrivateAttr() + + def has_next_page(self) -> bool: + items = self._get_page_items() + if not items: + return False + return self.next_page_info() is not None + + def next_page_info(self) -> Optional[PageInfo]: + ... + + def _get_page_items(self) -> Iterable[_T]: # type: ignore[empty-body] + ... + + def _params_from_url(self, url: URL) -> httpx.QueryParams: + # TODO: do we have to preprocess params here? + return httpx.QueryParams(cast(Any, self._options.params)).merge(url.params) + + def _info_to_options(self, info: PageInfo) -> FinalRequestOptions: + options = model_copy(self._options) + options._strip_raw_response_header() + + if not isinstance(info.params, NotGiven): + options.params = {**options.params, **info.params} + return options + + if not isinstance(info.url, NotGiven): + params = self._params_from_url(info.url) + url = info.url.copy_with(params=params) + options.params = dict(url.params) + options.url = str(url) + return options + + raise ValueError("Unexpected PageInfo state") + + +class BaseSyncPage(BasePage[_T], Generic[_T]): + _client: SyncAPIClient = pydantic.PrivateAttr() + + def _set_private_attributes( + self, + client: SyncAPIClient, + model: Type[_T], + options: FinalRequestOptions, + ) -> None: + self._model = model + self._client = client + self._options = options + + # Pydantic uses a custom `__iter__` method to support casting BaseModels + # to dictionaries. e.g. dict(model). + # As we want to support `for item in page`, this is inherently incompatible + # with the default pydantic behaviour. It is not possible to support both + # use cases at once. Fortunately, this is not a big deal as all other pydantic + # methods should continue to work as expected as there is an alternative method + # to cast a model to a dictionary, model.dict(), which is used internally + # by pydantic. + def __iter__(self) -> Iterator[_T]: # type: ignore + for page in self.iter_pages(): + for item in page._get_page_items(): + yield item + + def iter_pages(self: SyncPageT) -> Iterator[SyncPageT]: + page = self + while True: + yield page + if page.has_next_page(): + page = page.get_next_page() + else: + return + + def get_next_page(self: SyncPageT) -> SyncPageT: + info = self.next_page_info() + if not info: + raise RuntimeError( + "No next page expected; please check `.has_next_page()` before calling `.get_next_page()`." + ) + + options = self._info_to_options(info) + return self._client._request_api_list(self._model, page=self.__class__, options=options) + + +class AsyncPaginator(Generic[_T, AsyncPageT]): + def __init__( + self, + client: AsyncAPIClient, + options: FinalRequestOptions, + page_cls: Type[AsyncPageT], + model: Type[_T], + ) -> None: + self._model = model + self._client = client + self._options = options + self._page_cls = page_cls + + def __await__(self) -> Generator[Any, None, AsyncPageT]: + return self._get_page().__await__() + + async def _get_page(self) -> AsyncPageT: + def _parser(resp: AsyncPageT) -> AsyncPageT: + resp._set_private_attributes( + model=self._model, + options=self._options, + client=self._client, + ) + return resp + + self._options.post_parser = _parser + + return await self._client.request(self._page_cls, self._options) + + async def __aiter__(self) -> AsyncIterator[_T]: + # https://github.com/microsoft/pyright/issues/3464 + page = cast( + AsyncPageT, + await self, # type: ignore + ) + async for item in page: + yield item + + +class BaseAsyncPage(BasePage[_T], Generic[_T]): + _client: AsyncAPIClient = pydantic.PrivateAttr() + + def _set_private_attributes( + self, + model: Type[_T], + client: AsyncAPIClient, + options: FinalRequestOptions, + ) -> None: + self._model = model + self._client = client + self._options = options + + async def __aiter__(self) -> AsyncIterator[_T]: + async for page in self.iter_pages(): + for item in page._get_page_items(): + yield item + + async def iter_pages(self: AsyncPageT) -> AsyncIterator[AsyncPageT]: + page = self + while True: + yield page + if page.has_next_page(): + page = await page.get_next_page() + else: + return + + async def get_next_page(self: AsyncPageT) -> AsyncPageT: + info = self.next_page_info() + if not info: + raise RuntimeError( + "No next page expected; please check `.has_next_page()` before calling `.get_next_page()`." + ) + + options = self._info_to_options(info) + return await self._client._request_api_list(self._model, page=self.__class__, options=options) + + +_HttpxClientT = TypeVar("_HttpxClientT", bound=Union[httpx.Client, httpx.AsyncClient]) +_DefaultStreamT = TypeVar("_DefaultStreamT", bound=Union[Stream[Any], AsyncStream[Any]]) + + +class BaseClient(Generic[_HttpxClientT, _DefaultStreamT]): + _client: _HttpxClientT + _version: str + _base_url: URL + max_retries: int + timeout: Union[float, Timeout, None] + _limits: httpx.Limits + _proxies: ProxiesTypes | None + _transport: Transport | AsyncTransport | None + _strict_response_validation: bool + _idempotency_header: str | None + _default_stream_cls: type[_DefaultStreamT] | None = None + + def __init__( + self, + *, + version: str, + base_url: str | URL, + _strict_response_validation: bool, + max_retries: int = DEFAULT_MAX_RETRIES, + timeout: float | Timeout | None = DEFAULT_TIMEOUT, + limits: httpx.Limits, + transport: Transport | AsyncTransport | None, + proxies: ProxiesTypes | None, + custom_headers: Mapping[str, str] | None = None, + custom_query: Mapping[str, object] | None = None, + ) -> None: + self._version = version + self._base_url = self._enforce_trailing_slash(URL(base_url)) + self.max_retries = max_retries + self.timeout = timeout + self._limits = limits + self._proxies = proxies + self._transport = transport + self._custom_headers = custom_headers or {} + self._custom_query = custom_query or {} + self._strict_response_validation = _strict_response_validation + self._idempotency_header = None + + if max_retries is None: # pyright: ignore[reportUnnecessaryComparison] + raise TypeError( + "max_retries cannot be None. If you want to disable retries, pass `0`; if you want unlimited retries, pass `math.inf` or a very high number; if you want the default behavior, pass `writerai.DEFAULT_MAX_RETRIES`" + ) + + def _enforce_trailing_slash(self, url: URL) -> URL: + if url.raw_path.endswith(b"/"): + return url + return url.copy_with(raw_path=url.raw_path + b"/") + + def _make_status_error_from_response( + self, + response: httpx.Response, + ) -> APIStatusError: + if response.is_closed and not response.is_stream_consumed: + # We can't read the response body as it has been closed + # before it was read. This can happen if an event hook + # raises a status error. + body = None + err_msg = f"Error code: {response.status_code}" + else: + err_text = response.text.strip() + body = err_text + + try: + body = json.loads(err_text) + err_msg = f"Error code: {response.status_code} - {body}" + except Exception: + err_msg = err_text or f"Error code: {response.status_code}" + + return self._make_status_error(err_msg, body=body, response=response) + + def _make_status_error( + self, + err_msg: str, + *, + body: object, + response: httpx.Response, + ) -> _exceptions.APIStatusError: + raise NotImplementedError() + + def _remaining_retries( + self, + remaining_retries: Optional[int], + options: FinalRequestOptions, + ) -> int: + return remaining_retries if remaining_retries is not None else options.get_max_retries(self.max_retries) + + def _build_headers(self, options: FinalRequestOptions) -> httpx.Headers: + custom_headers = options.headers or {} + headers_dict = _merge_mappings(self.default_headers, custom_headers) + self._validate_headers(headers_dict, custom_headers) + + # headers are case-insensitive while dictionaries are not. + headers = httpx.Headers(headers_dict) + + idempotency_header = self._idempotency_header + if idempotency_header and options.method.lower() != "get" and idempotency_header not in headers: + headers[idempotency_header] = options.idempotency_key or self._idempotency_key() + + return headers + + def _prepare_url(self, url: str) -> URL: + """ + Merge a URL argument together with any 'base_url' on the client, + to create the URL used for the outgoing request. + """ + # Copied from httpx's `_merge_url` method. + merge_url = URL(url) + if merge_url.is_relative_url: + merge_raw_path = self.base_url.raw_path + merge_url.raw_path.lstrip(b"/") + return self.base_url.copy_with(raw_path=merge_raw_path) + + return merge_url + + def _make_sse_decoder(self) -> SSEDecoder | SSEBytesDecoder: + return SSEDecoder() + + def _build_request( + self, + options: FinalRequestOptions, + ) -> httpx.Request: + if log.isEnabledFor(logging.DEBUG): + log.debug("Request options: %s", model_dump(options, exclude_unset=True)) + + kwargs: dict[str, Any] = {} + + json_data = options.json_data + if options.extra_json is not None: + if json_data is None: + json_data = cast(Body, options.extra_json) + elif is_mapping(json_data): + json_data = _merge_mappings(json_data, options.extra_json) + else: + raise RuntimeError(f"Unexpected JSON data type, {type(json_data)}, cannot merge with `extra_body`") + + headers = self._build_headers(options) + params = _merge_mappings(self._custom_query, options.params) + content_type = headers.get("Content-Type") + + # If the given Content-Type header is multipart/form-data then it + # has to be removed so that httpx can generate the header with + # additional information for us as it has to be in this form + # for the server to be able to correctly parse the request: + # multipart/form-data; boundary=---abc-- + if content_type is not None and content_type.startswith("multipart/form-data"): + if "boundary" not in content_type: + # only remove the header if the boundary hasn't been explicitly set + # as the caller doesn't want httpx to come up with their own boundary + headers.pop("Content-Type") + + # As we are now sending multipart/form-data instead of application/json + # we need to tell httpx to use it, https://www.python-httpx.org/advanced/#multipart-file-encoding + if json_data: + if not is_dict(json_data): + raise TypeError( + f"Expected query input to be a dictionary for multipart requests but got {type(json_data)} instead." + ) + kwargs["data"] = self._serialize_multipartform(json_data) + + # TODO: report this error to httpx + return self._client.build_request( # pyright: ignore[reportUnknownMemberType] + headers=headers, + timeout=self.timeout if isinstance(options.timeout, NotGiven) else options.timeout, + method=options.method, + url=self._prepare_url(options.url), + # the `Query` type that we use is incompatible with qs' + # `Params` type as it needs to be typed as `Mapping[str, object]` + # so that passing a `TypedDict` doesn't cause an error. + # https://github.com/microsoft/pyright/issues/3526#event-6715453066 + params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None, + json=json_data, + files=options.files, + **kwargs, + ) + + def _serialize_multipartform(self, data: Mapping[object, object]) -> dict[str, object]: + items = self.qs.stringify_items( + # TODO: type ignore is required as stringify_items is well typed but we can't be + # well typed without heavy validation. + data, # type: ignore + array_format="brackets", + ) + serialized: dict[str, object] = {} + for key, value in items: + existing = serialized.get(key) + + if not existing: + serialized[key] = value + continue + + # If a value has already been set for this key then that + # means we're sending data like `array[]=[1, 2, 3]` and we + # need to tell httpx that we want to send multiple values with + # the same key which is done by using a list or a tuple. + # + # Note: 2d arrays should never result in the same key at both + # levels so it's safe to assume that if the value is a list, + # it was because we changed it to be a list. + if is_list(existing): + existing.append(value) + else: + serialized[key] = [existing, value] + + return serialized + + def _maybe_override_cast_to(self, cast_to: type[ResponseT], options: FinalRequestOptions) -> type[ResponseT]: + if not is_given(options.headers): + return cast_to + + # make a copy of the headers so we don't mutate user-input + headers = dict(options.headers) + + # we internally support defining a temporary header to override the + # default `cast_to` type for use with `.with_raw_response` and `.with_streaming_response` + # see _response.py for implementation details + override_cast_to = headers.pop(OVERRIDE_CAST_TO_HEADER, NOT_GIVEN) + if is_given(override_cast_to): + options.headers = headers + return cast(Type[ResponseT], override_cast_to) + + return cast_to + + def _should_stream_response_body(self, request: httpx.Request) -> bool: + return request.headers.get(RAW_RESPONSE_HEADER) == "stream" # type: ignore[no-any-return] + + def _process_response_data( + self, + *, + data: object, + cast_to: type[ResponseT], + response: httpx.Response, + ) -> ResponseT: + if data is None: + return cast(ResponseT, None) + + if cast_to is object: + return cast(ResponseT, data) + + try: + if inspect.isclass(cast_to) and issubclass(cast_to, ModelBuilderProtocol): + return cast(ResponseT, cast_to.build(response=response, data=data)) + + if self._strict_response_validation: + return cast(ResponseT, validate_type(type_=cast_to, value=data)) + + return cast(ResponseT, construct_type(type_=cast_to, value=data)) + except pydantic.ValidationError as err: + raise APIResponseValidationError(response=response, body=data) from err + + @property + def qs(self) -> Querystring: + return Querystring() + + @property + def custom_auth(self) -> httpx.Auth | None: + return None + + @property + def auth_headers(self) -> dict[str, str]: + return {} + + @property + def default_headers(self) -> dict[str, str | Omit]: + return { + "Accept": "application/json", + "Content-Type": "application/json", + "User-Agent": self.user_agent, + **self.platform_headers(), + **self.auth_headers, + **self._custom_headers, + } + + def _validate_headers( + self, + headers: Headers, # noqa: ARG002 + custom_headers: Headers, # noqa: ARG002 + ) -> None: + """Validate the given default headers and custom headers. + + Does nothing by default. + """ + return + + @property + def user_agent(self) -> str: + return f"{self.__class__.__name__}/Python {self._version}" + + @property + def base_url(self) -> URL: + return self._base_url + + @base_url.setter + def base_url(self, url: URL | str) -> None: + self._base_url = self._enforce_trailing_slash(url if isinstance(url, URL) else URL(url)) + + def platform_headers(self) -> Dict[str, str]: + return platform_headers(self._version) + + def _parse_retry_after_header(self, response_headers: Optional[httpx.Headers] = None) -> float | None: + """Returns a float of the number of seconds (not milliseconds) to wait after retrying, or None if unspecified. + + About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After + See also https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After#syntax + """ + if response_headers is None: + return None + + # First, try the non-standard `retry-after-ms` header for milliseconds, + # which is more precise than integer-seconds `retry-after` + try: + retry_ms_header = response_headers.get("retry-after-ms", None) + return float(retry_ms_header) / 1000 + except (TypeError, ValueError): + pass + + # Next, try parsing `retry-after` header as seconds (allowing nonstandard floats). + retry_header = response_headers.get("retry-after") + try: + # note: the spec indicates that this should only ever be an integer + # but if someone sends a float there's no reason for us to not respect it + return float(retry_header) + except (TypeError, ValueError): + pass + + # Last, try parsing `retry-after` as a date. + retry_date_tuple = email.utils.parsedate_tz(retry_header) + if retry_date_tuple is None: + return None + + retry_date = email.utils.mktime_tz(retry_date_tuple) + return float(retry_date - time.time()) + + def _calculate_retry_timeout( + self, + remaining_retries: int, + options: FinalRequestOptions, + response_headers: Optional[httpx.Headers] = None, + ) -> float: + max_retries = options.get_max_retries(self.max_retries) + + # If the API asks us to wait a certain amount of time (and it's a reasonable amount), just do what it says. + retry_after = self._parse_retry_after_header(response_headers) + if retry_after is not None and 0 < retry_after <= 60: + return retry_after + + nb_retries = max_retries - remaining_retries + + # Apply exponential backoff, but not more than the max. + sleep_seconds = min(INITIAL_RETRY_DELAY * pow(2.0, nb_retries), MAX_RETRY_DELAY) + + # Apply some jitter, plus-or-minus half a second. + jitter = 1 - 0.25 * random() + timeout = sleep_seconds * jitter + return timeout if timeout >= 0 else 0 + + def _should_retry(self, response: httpx.Response) -> bool: + # Note: this is not a standard header + should_retry_header = response.headers.get("x-should-retry") + + # If the server explicitly says whether or not to retry, obey. + if should_retry_header == "true": + log.debug("Retrying as header `x-should-retry` is set to `true`") + return True + if should_retry_header == "false": + log.debug("Not retrying as header `x-should-retry` is set to `false`") + return False + + # Retry on request timeouts. + if response.status_code == 408: + log.debug("Retrying due to status code %i", response.status_code) + return True + + # Retry on lock timeouts. + if response.status_code == 409: + log.debug("Retrying due to status code %i", response.status_code) + return True + + # Retry on rate limits. + if response.status_code == 429: + log.debug("Retrying due to status code %i", response.status_code) + return True + + # Retry internal errors. + if response.status_code >= 500: + log.debug("Retrying due to status code %i", response.status_code) + return True + + log.debug("Not retrying") + return False + + def _idempotency_key(self) -> str: + return f"stainless-python-retry-{uuid.uuid4()}" + + +class _DefaultHttpxClient(httpx.Client): + def __init__(self, **kwargs: Any) -> None: + kwargs.setdefault("timeout", DEFAULT_TIMEOUT) + kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) + kwargs.setdefault("follow_redirects", True) + super().__init__(**kwargs) + + +if TYPE_CHECKING: + DefaultHttpxClient = httpx.Client + """An alias to `httpx.Client` that provides the same defaults that this SDK + uses internally. + + This is useful because overriding the `http_client` with your own instance of + `httpx.Client` will result in httpx's defaults being used, not ours. + """ +else: + DefaultHttpxClient = _DefaultHttpxClient + + +class SyncHttpxClientWrapper(DefaultHttpxClient): + def __del__(self) -> None: + try: + self.close() + except Exception: + pass + + +class SyncAPIClient(BaseClient[httpx.Client, Stream[Any]]): + _client: httpx.Client + _default_stream_cls: type[Stream[Any]] | None = None + + def __init__( + self, + *, + version: str, + base_url: str | URL, + max_retries: int = DEFAULT_MAX_RETRIES, + timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + transport: Transport | None = None, + proxies: ProxiesTypes | None = None, + limits: Limits | None = None, + http_client: httpx.Client | None = None, + custom_headers: Mapping[str, str] | None = None, + custom_query: Mapping[str, object] | None = None, + _strict_response_validation: bool, + ) -> None: + if limits is not None: + warnings.warn( + "The `connection_pool_limits` argument is deprecated. The `http_client` argument should be passed instead", + category=DeprecationWarning, + stacklevel=3, + ) + if http_client is not None: + raise ValueError("The `http_client` argument is mutually exclusive with `connection_pool_limits`") + else: + limits = DEFAULT_CONNECTION_LIMITS + + if transport is not None: + warnings.warn( + "The `transport` argument is deprecated. The `http_client` argument should be passed instead", + category=DeprecationWarning, + stacklevel=3, + ) + if http_client is not None: + raise ValueError("The `http_client` argument is mutually exclusive with `transport`") + + if proxies is not None: + warnings.warn( + "The `proxies` argument is deprecated. The `http_client` argument should be passed instead", + category=DeprecationWarning, + stacklevel=3, + ) + if http_client is not None: + raise ValueError("The `http_client` argument is mutually exclusive with `proxies`") + + if not is_given(timeout): + # if the user passed in a custom http client with a non-default + # timeout set then we use that timeout. + # + # note: there is an edge case here where the user passes in a client + # where they've explicitly set the timeout to match the default timeout + # as this check is structural, meaning that we'll think they didn't + # pass in a timeout and will ignore it + if http_client and http_client.timeout != HTTPX_DEFAULT_TIMEOUT: + timeout = http_client.timeout + else: + timeout = DEFAULT_TIMEOUT + + if http_client is not None and not isinstance(http_client, httpx.Client): # pyright: ignore[reportUnnecessaryIsInstance] + raise TypeError( + f"Invalid `http_client` argument; Expected an instance of `httpx.Client` but got {type(http_client)}" + ) + + super().__init__( + version=version, + limits=limits, + # cast to a valid type because mypy doesn't understand our type narrowing + timeout=cast(Timeout, timeout), + proxies=proxies, + base_url=base_url, + transport=transport, + max_retries=max_retries, + custom_query=custom_query, + custom_headers=custom_headers, + _strict_response_validation=_strict_response_validation, + ) + self._client = http_client or SyncHttpxClientWrapper( + base_url=base_url, + # cast to a valid type because mypy doesn't understand our type narrowing + timeout=cast(Timeout, timeout), + proxies=proxies, + transport=transport, + limits=limits, + follow_redirects=True, + ) + + def is_closed(self) -> bool: + return self._client.is_closed + + def close(self) -> None: + """Close the underlying HTTPX client. + + The client will *not* be usable after this. + """ + # If an error is thrown while constructing a client, self._client + # may not be present + if hasattr(self, "_client"): + self._client.close() + + def __enter__(self: _T) -> _T: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.close() + + def _prepare_options( + self, + options: FinalRequestOptions, # noqa: ARG002 + ) -> None: + """Hook for mutating the given options""" + return None + + def _prepare_request( + self, + request: httpx.Request, # noqa: ARG002 + ) -> None: + """This method is used as a callback for mutating the `Request` object + after it has been constructed. + This is useful for cases where you want to add certain headers based off of + the request properties, e.g. `url`, `method` etc. + """ + return None + + @overload + def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + remaining_retries: Optional[int] = None, + *, + stream: Literal[True], + stream_cls: Type[_StreamT], + ) -> _StreamT: + ... + + @overload + def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + remaining_retries: Optional[int] = None, + *, + stream: Literal[False] = False, + ) -> ResponseT: + ... + + @overload + def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + remaining_retries: Optional[int] = None, + *, + stream: bool = False, + stream_cls: Type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: + ... + + def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + remaining_retries: Optional[int] = None, + *, + stream: bool = False, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: + return self._request( + cast_to=cast_to, + options=options, + stream=stream, + stream_cls=stream_cls, + remaining_retries=remaining_retries, + ) + + def _request( + self, + *, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + remaining_retries: int | None, + stream: bool, + stream_cls: type[_StreamT] | None, + ) -> ResponseT | _StreamT: + cast_to = self._maybe_override_cast_to(cast_to, options) + self._prepare_options(options) + + retries = self._remaining_retries(remaining_retries, options) + request = self._build_request(options) + self._prepare_request(request) + + kwargs: HttpxSendArgs = {} + if self.custom_auth is not None: + kwargs["auth"] = self.custom_auth + + log.debug("Sending HTTP Request: %s %s", request.method, request.url) + + try: + response = self._client.send( + request, + stream=stream or self._should_stream_response_body(request=request), + **kwargs, + ) + except httpx.TimeoutException as err: + log.debug("Encountered httpx.TimeoutException", exc_info=True) + + if retries > 0: + return self._retry_request( + options, + cast_to, + retries, + stream=stream, + stream_cls=stream_cls, + response_headers=None, + ) + + log.debug("Raising timeout error") + raise APITimeoutError(request=request) from err + except Exception as err: + log.debug("Encountered Exception", exc_info=True) + + if retries > 0: + return self._retry_request( + options, + cast_to, + retries, + stream=stream, + stream_cls=stream_cls, + response_headers=None, + ) + + log.debug("Raising connection error") + raise APIConnectionError(request=request) from err + + log.debug( + 'HTTP Response: %s %s "%i %s" %s', + request.method, + request.url, + response.status_code, + response.reason_phrase, + response.headers, + ) + + try: + response.raise_for_status() + except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code + log.debug("Encountered httpx.HTTPStatusError", exc_info=True) + + if retries > 0 and self._should_retry(err.response): + err.response.close() + return self._retry_request( + options, + cast_to, + retries, + err.response.headers, + stream=stream, + stream_cls=stream_cls, + ) + + # If the response is streamed then we need to explicitly read the response + # to completion before attempting to access the response text. + if not err.response.is_closed: + err.response.read() + + log.debug("Re-raising status error") + raise self._make_status_error_from_response(err.response) from None + + return self._process_response( + cast_to=cast_to, + options=options, + response=response, + stream=stream, + stream_cls=stream_cls, + ) + + def _retry_request( + self, + options: FinalRequestOptions, + cast_to: Type[ResponseT], + remaining_retries: int, + response_headers: httpx.Headers | None, + *, + stream: bool, + stream_cls: type[_StreamT] | None, + ) -> ResponseT | _StreamT: + remaining = remaining_retries - 1 + if remaining == 1: + log.debug("1 retry left") + else: + log.debug("%i retries left", remaining) + + timeout = self._calculate_retry_timeout(remaining, options, response_headers) + log.info("Retrying request to %s in %f seconds", options.url, timeout) + + # In a synchronous context we are blocking the entire thread. Up to the library user to run the client in a + # different thread if necessary. + time.sleep(timeout) + + return self._request( + options=options, + cast_to=cast_to, + remaining_retries=remaining, + stream=stream, + stream_cls=stream_cls, + ) + + def _process_response( + self, + *, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + response: httpx.Response, + stream: bool, + stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, + ) -> ResponseT: + origin = get_origin(cast_to) or cast_to + + if inspect.isclass(origin) and issubclass(origin, BaseAPIResponse): + if not issubclass(origin, APIResponse): + raise TypeError(f"API Response types must subclass {APIResponse}; Received {origin}") + + response_cls = cast("type[BaseAPIResponse[Any]]", cast_to) + return cast( + ResponseT, + response_cls( + raw=response, + client=self, + cast_to=extract_response_type(response_cls), + stream=stream, + stream_cls=stream_cls, + options=options, + ), + ) + + if cast_to == httpx.Response: + return cast(ResponseT, response) + + api_response = APIResponse( + raw=response, + client=self, + cast_to=cast("type[ResponseT]", cast_to), # pyright: ignore[reportUnnecessaryCast] + stream=stream, + stream_cls=stream_cls, + options=options, + ) + if bool(response.request.headers.get(RAW_RESPONSE_HEADER)): + return cast(ResponseT, api_response) + + return api_response.parse() + + def _request_api_list( + self, + model: Type[object], + page: Type[SyncPageT], + options: FinalRequestOptions, + ) -> SyncPageT: + def _parser(resp: SyncPageT) -> SyncPageT: + resp._set_private_attributes( + client=self, + model=model, + options=options, + ) + return resp + + options.post_parser = _parser + + return self.request(page, options, stream=False) + + @overload + def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: Literal[False] = False, + ) -> ResponseT: + ... + + @overload + def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: Literal[True], + stream_cls: type[_StreamT], + ) -> _StreamT: + ... + + @overload + def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: bool, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: + ... + + def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: bool = False, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: + opts = FinalRequestOptions.construct(method="get", url=path, **options) + # cast is required because mypy complains about returning Any even though + # it understands the type variables + return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) + + @overload + def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + files: RequestFiles | None = None, + stream: Literal[False] = False, + ) -> ResponseT: + ... + + @overload + def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + files: RequestFiles | None = None, + stream: Literal[True], + stream_cls: type[_StreamT], + ) -> _StreamT: + ... + + @overload + def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + files: RequestFiles | None = None, + stream: bool, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: + ... + + def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + files: RequestFiles | None = None, + stream: bool = False, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: + opts = FinalRequestOptions.construct( + method="post", url=path, json_data=body, files=to_httpx_files(files), **options + ) + return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) + + def patch( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + return self.request(cast_to, opts) + + def put( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + opts = FinalRequestOptions.construct( + method="put", url=path, json_data=body, files=to_httpx_files(files), **options + ) + return self.request(cast_to, opts) + + def delete( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + return self.request(cast_to, opts) + + def get_api_list( + self, + path: str, + *, + model: Type[object], + page: Type[SyncPageT], + body: Body | None = None, + options: RequestOptions = {}, + method: str = "get", + ) -> SyncPageT: + opts = FinalRequestOptions.construct(method=method, url=path, json_data=body, **options) + return self._request_api_list(model, page, opts) + + +class _DefaultAsyncHttpxClient(httpx.AsyncClient): + def __init__(self, **kwargs: Any) -> None: + kwargs.setdefault("timeout", DEFAULT_TIMEOUT) + kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) + kwargs.setdefault("follow_redirects", True) + super().__init__(**kwargs) + + +if TYPE_CHECKING: + DefaultAsyncHttpxClient = httpx.AsyncClient + """An alias to `httpx.AsyncClient` that provides the same defaults that this SDK + uses internally. + + This is useful because overriding the `http_client` with your own instance of + `httpx.AsyncClient` will result in httpx's defaults being used, not ours. + """ +else: + DefaultAsyncHttpxClient = _DefaultAsyncHttpxClient + + +class AsyncHttpxClientWrapper(DefaultAsyncHttpxClient): + def __del__(self) -> None: + try: + # TODO(someday): support non asyncio runtimes here + asyncio.get_running_loop().create_task(self.aclose()) + except Exception: + pass + + +class AsyncAPIClient(BaseClient[httpx.AsyncClient, AsyncStream[Any]]): + _client: httpx.AsyncClient + _default_stream_cls: type[AsyncStream[Any]] | None = None + + def __init__( + self, + *, + version: str, + base_url: str | URL, + _strict_response_validation: bool, + max_retries: int = DEFAULT_MAX_RETRIES, + timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + transport: AsyncTransport | None = None, + proxies: ProxiesTypes | None = None, + limits: Limits | None = None, + http_client: httpx.AsyncClient | None = None, + custom_headers: Mapping[str, str] | None = None, + custom_query: Mapping[str, object] | None = None, + ) -> None: + if limits is not None: + warnings.warn( + "The `connection_pool_limits` argument is deprecated. The `http_client` argument should be passed instead", + category=DeprecationWarning, + stacklevel=3, + ) + if http_client is not None: + raise ValueError("The `http_client` argument is mutually exclusive with `connection_pool_limits`") + else: + limits = DEFAULT_CONNECTION_LIMITS + + if transport is not None: + warnings.warn( + "The `transport` argument is deprecated. The `http_client` argument should be passed instead", + category=DeprecationWarning, + stacklevel=3, + ) + if http_client is not None: + raise ValueError("The `http_client` argument is mutually exclusive with `transport`") + + if proxies is not None: + warnings.warn( + "The `proxies` argument is deprecated. The `http_client` argument should be passed instead", + category=DeprecationWarning, + stacklevel=3, + ) + if http_client is not None: + raise ValueError("The `http_client` argument is mutually exclusive with `proxies`") + + if not is_given(timeout): + # if the user passed in a custom http client with a non-default + # timeout set then we use that timeout. + # + # note: there is an edge case here where the user passes in a client + # where they've explicitly set the timeout to match the default timeout + # as this check is structural, meaning that we'll think they didn't + # pass in a timeout and will ignore it + if http_client and http_client.timeout != HTTPX_DEFAULT_TIMEOUT: + timeout = http_client.timeout + else: + timeout = DEFAULT_TIMEOUT + + if http_client is not None and not isinstance(http_client, httpx.AsyncClient): # pyright: ignore[reportUnnecessaryIsInstance] + raise TypeError( + f"Invalid `http_client` argument; Expected an instance of `httpx.AsyncClient` but got {type(http_client)}" + ) + + super().__init__( + version=version, + base_url=base_url, + limits=limits, + # cast to a valid type because mypy doesn't understand our type narrowing + timeout=cast(Timeout, timeout), + proxies=proxies, + transport=transport, + max_retries=max_retries, + custom_query=custom_query, + custom_headers=custom_headers, + _strict_response_validation=_strict_response_validation, + ) + self._client = http_client or AsyncHttpxClientWrapper( + base_url=base_url, + # cast to a valid type because mypy doesn't understand our type narrowing + timeout=cast(Timeout, timeout), + proxies=proxies, + transport=transport, + limits=limits, + follow_redirects=True, + ) + + def is_closed(self) -> bool: + return self._client.is_closed + + async def close(self) -> None: + """Close the underlying HTTPX client. + + The client will *not* be usable after this. + """ + await self._client.aclose() + + async def __aenter__(self: _T) -> _T: + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + await self.close() + + async def _prepare_options( + self, + options: FinalRequestOptions, # noqa: ARG002 + ) -> None: + """Hook for mutating the given options""" + return None + + async def _prepare_request( + self, + request: httpx.Request, # noqa: ARG002 + ) -> None: + """This method is used as a callback for mutating the `Request` object + after it has been constructed. + This is useful for cases where you want to add certain headers based off of + the request properties, e.g. `url`, `method` etc. + """ + return None + + @overload + async def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: Literal[False] = False, + remaining_retries: Optional[int] = None, + ) -> ResponseT: + ... + + @overload + async def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: Literal[True], + stream_cls: type[_AsyncStreamT], + remaining_retries: Optional[int] = None, + ) -> _AsyncStreamT: + ... + + @overload + async def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: bool, + stream_cls: type[_AsyncStreamT] | None = None, + remaining_retries: Optional[int] = None, + ) -> ResponseT | _AsyncStreamT: + ... + + async def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: bool = False, + stream_cls: type[_AsyncStreamT] | None = None, + remaining_retries: Optional[int] = None, + ) -> ResponseT | _AsyncStreamT: + return await self._request( + cast_to=cast_to, + options=options, + stream=stream, + stream_cls=stream_cls, + remaining_retries=remaining_retries, + ) + + async def _request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: bool, + stream_cls: type[_AsyncStreamT] | None, + remaining_retries: int | None, + ) -> ResponseT | _AsyncStreamT: + cast_to = self._maybe_override_cast_to(cast_to, options) + await self._prepare_options(options) + + retries = self._remaining_retries(remaining_retries, options) + request = self._build_request(options) + await self._prepare_request(request) + + kwargs: HttpxSendArgs = {} + if self.custom_auth is not None: + kwargs["auth"] = self.custom_auth + + try: + response = await self._client.send( + request, + stream=stream or self._should_stream_response_body(request=request), + **kwargs, + ) + except httpx.TimeoutException as err: + log.debug("Encountered httpx.TimeoutException", exc_info=True) + + if retries > 0: + return await self._retry_request( + options, + cast_to, + retries, + stream=stream, + stream_cls=stream_cls, + response_headers=None, + ) + + log.debug("Raising timeout error") + raise APITimeoutError(request=request) from err + except Exception as err: + log.debug("Encountered Exception", exc_info=True) + + if retries > 0: + return await self._retry_request( + options, + cast_to, + retries, + stream=stream, + stream_cls=stream_cls, + response_headers=None, + ) + + log.debug("Raising connection error") + raise APIConnectionError(request=request) from err + + log.debug( + 'HTTP Request: %s %s "%i %s"', request.method, request.url, response.status_code, response.reason_phrase + ) + + try: + response.raise_for_status() + except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code + log.debug("Encountered httpx.HTTPStatusError", exc_info=True) + + if retries > 0 and self._should_retry(err.response): + await err.response.aclose() + return await self._retry_request( + options, + cast_to, + retries, + err.response.headers, + stream=stream, + stream_cls=stream_cls, + ) + + # If the response is streamed then we need to explicitly read the response + # to completion before attempting to access the response text. + if not err.response.is_closed: + await err.response.aread() + + log.debug("Re-raising status error") + raise self._make_status_error_from_response(err.response) from None + + return await self._process_response( + cast_to=cast_to, + options=options, + response=response, + stream=stream, + stream_cls=stream_cls, + ) + + async def _retry_request( + self, + options: FinalRequestOptions, + cast_to: Type[ResponseT], + remaining_retries: int, + response_headers: httpx.Headers | None, + *, + stream: bool, + stream_cls: type[_AsyncStreamT] | None, + ) -> ResponseT | _AsyncStreamT: + remaining = remaining_retries - 1 + if remaining == 1: + log.debug("1 retry left") + else: + log.debug("%i retries left", remaining) + + timeout = self._calculate_retry_timeout(remaining, options, response_headers) + log.info("Retrying request to %s in %f seconds", options.url, timeout) + + await anyio.sleep(timeout) + + return await self._request( + options=options, + cast_to=cast_to, + remaining_retries=remaining, + stream=stream, + stream_cls=stream_cls, + ) + + async def _process_response( + self, + *, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + response: httpx.Response, + stream: bool, + stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, + ) -> ResponseT: + origin = get_origin(cast_to) or cast_to + + if inspect.isclass(origin) and issubclass(origin, BaseAPIResponse): + if not issubclass(origin, AsyncAPIResponse): + raise TypeError(f"API Response types must subclass {AsyncAPIResponse}; Received {origin}") + + response_cls = cast("type[BaseAPIResponse[Any]]", cast_to) + return cast( + "ResponseT", + response_cls( + raw=response, + client=self, + cast_to=extract_response_type(response_cls), + stream=stream, + stream_cls=stream_cls, + options=options, + ), + ) + + if cast_to == httpx.Response: + return cast(ResponseT, response) + + api_response = AsyncAPIResponse( + raw=response, + client=self, + cast_to=cast("type[ResponseT]", cast_to), # pyright: ignore[reportUnnecessaryCast] + stream=stream, + stream_cls=stream_cls, + options=options, + ) + if bool(response.request.headers.get(RAW_RESPONSE_HEADER)): + return cast(ResponseT, api_response) + + return await api_response.parse() + + def _request_api_list( + self, + model: Type[_T], + page: Type[AsyncPageT], + options: FinalRequestOptions, + ) -> AsyncPaginator[_T, AsyncPageT]: + return AsyncPaginator(client=self, options=options, page_cls=page, model=model) + + @overload + async def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: Literal[False] = False, + ) -> ResponseT: + ... + + @overload + async def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: Literal[True], + stream_cls: type[_AsyncStreamT], + ) -> _AsyncStreamT: + ... + + @overload + async def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: bool, + stream_cls: type[_AsyncStreamT] | None = None, + ) -> ResponseT | _AsyncStreamT: + ... + + async def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: bool = False, + stream_cls: type[_AsyncStreamT] | None = None, + ) -> ResponseT | _AsyncStreamT: + opts = FinalRequestOptions.construct(method="get", url=path, **options) + return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) + + @overload + async def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + stream: Literal[False] = False, + ) -> ResponseT: + ... + + @overload + async def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + stream: Literal[True], + stream_cls: type[_AsyncStreamT], + ) -> _AsyncStreamT: + ... + + @overload + async def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + stream: bool, + stream_cls: type[_AsyncStreamT] | None = None, + ) -> ResponseT | _AsyncStreamT: + ... + + async def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + stream: bool = False, + stream_cls: type[_AsyncStreamT] | None = None, + ) -> ResponseT | _AsyncStreamT: + opts = FinalRequestOptions.construct( + method="post", url=path, json_data=body, files=await async_to_httpx_files(files), **options + ) + return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) + + async def patch( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + return await self.request(cast_to, opts) + + async def put( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + opts = FinalRequestOptions.construct( + method="put", url=path, json_data=body, files=await async_to_httpx_files(files), **options + ) + return await self.request(cast_to, opts) + + async def delete( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + return await self.request(cast_to, opts) + + def get_api_list( + self, + path: str, + *, + model: Type[_T], + page: Type[AsyncPageT], + body: Body | None = None, + options: RequestOptions = {}, + method: str = "get", + ) -> AsyncPaginator[_T, AsyncPageT]: + opts = FinalRequestOptions.construct(method=method, url=path, json_data=body, **options) + return self._request_api_list(model, page, opts) + + +def make_request_options( + *, + query: Query | None = None, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + idempotency_key: str | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + post_parser: PostParser | NotGiven = NOT_GIVEN, +) -> RequestOptions: + """Create a dict of type RequestOptions without keys of NotGiven values.""" + options: RequestOptions = {} + if extra_headers is not None: + options["headers"] = extra_headers + + if extra_body is not None: + options["extra_json"] = cast(AnyMapping, extra_body) + + if query is not None: + options["params"] = query + + if extra_query is not None: + options["params"] = {**options.get("params", {}), **extra_query} + + if not isinstance(timeout, NotGiven): + options["timeout"] = timeout + + if idempotency_key is not None: + options["idempotency_key"] = idempotency_key + + if is_given(post_parser): + # internal + options["post_parser"] = post_parser # type: ignore + + return options + + +class OtherPlatform: + def __init__(self, name: str) -> None: + self.name = name + + @override + def __str__(self) -> str: + return f"Other:{self.name}" + + +Platform = Union[ + OtherPlatform, + Literal[ + "MacOS", + "Linux", + "Windows", + "FreeBSD", + "OpenBSD", + "iOS", + "Android", + "Unknown", + ], +] + + +def get_platform() -> Platform: + try: + system = platform.system().lower() + platform_name = platform.platform().lower() + except Exception: + return "Unknown" + + if "iphone" in platform_name or "ipad" in platform_name: + # Tested using Python3IDE on an iPhone 11 and Pythonista on an iPad 7 + # system is Darwin and platform_name is a string like: + # - Darwin-21.6.0-iPhone12,1-64bit + # - Darwin-21.6.0-iPad7,11-64bit + return "iOS" + + if system == "darwin": + return "MacOS" + + if system == "windows": + return "Windows" + + if "android" in platform_name: + # Tested using Pydroid 3 + # system is Linux and platform_name is a string like 'Linux-5.10.81-android12-9-00001-geba40aecb3b7-ab8534902-aarch64-with-libc' + return "Android" + + if system == "linux": + # https://distro.readthedocs.io/en/latest/#distro.id + distro_id = distro.id() + if distro_id == "freebsd": + return "FreeBSD" + + if distro_id == "openbsd": + return "OpenBSD" + + return "Linux" + + if platform_name: + return OtherPlatform(platform_name) + + return "Unknown" + + +@lru_cache(maxsize=None) +def platform_headers(version: str) -> Dict[str, str]: + return { + "X-Stainless-Lang": "python", + "X-Stainless-Package-Version": version, + "X-Stainless-OS": str(get_platform()), + "X-Stainless-Arch": str(get_architecture()), + "X-Stainless-Runtime": get_python_runtime(), + "X-Stainless-Runtime-Version": get_python_version(), + } + + +class OtherArch: + def __init__(self, name: str) -> None: + self.name = name + + @override + def __str__(self) -> str: + return f"other:{self.name}" + + +Arch = Union[OtherArch, Literal["x32", "x64", "arm", "arm64", "unknown"]] + + +def get_python_runtime() -> str: + try: + return platform.python_implementation() + except Exception: + return "unknown" + + +def get_python_version() -> str: + try: + return platform.python_version() + except Exception: + return "unknown" + + +def get_architecture() -> Arch: + try: + python_bitness, _ = platform.architecture() + machine = platform.machine().lower() + except Exception: + return "unknown" + + if machine in ("arm64", "aarch64"): + return "arm64" + + # TODO: untested + if machine == "arm": + return "arm" + + if machine == "x86_64": + return "x64" + + # TODO: untested + if python_bitness == "32bit": + return "x32" + + if machine: + return OtherArch(machine) + + return "unknown" + + +def _merge_mappings( + obj1: Mapping[_T_co, Union[_T, Omit]], + obj2: Mapping[_T_co, Union[_T, Omit]], +) -> Dict[_T_co, _T]: + """Merge two mappings of the same type, removing any values that are instances of `Omit`. + + In cases with duplicate keys the second mapping takes precedence. + """ + merged = {**obj1, **obj2} + return {key: value for key, value in merged.items() if not isinstance(value, Omit)} diff --git a/src/writer_ai/_client.py b/src/writer_ai/_client.py new file mode 100644 index 00000000..4bf2e328 --- /dev/null +++ b/src/writer_ai/_client.py @@ -0,0 +1,426 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, Union, Mapping +from typing_extensions import Self, override + +import httpx + +from . import resources, _exceptions +from ._qs import Querystring +from ._types import ( + NOT_GIVEN, + Omit, + Timeout, + NotGiven, + Transport, + ProxiesTypes, + RequestOptions, +) +from ._utils import ( + is_given, + get_async_library, +) +from ._version import __version__ +from ._streaming import Stream as Stream, AsyncStream as AsyncStream +from ._exceptions import WriterAIError, APIStatusError +from ._base_client import ( + DEFAULT_MAX_RETRIES, + SyncAPIClient, + AsyncAPIClient, +) + +__all__ = [ + "Timeout", + "Transport", + "ProxiesTypes", + "RequestOptions", + "resources", + "WriterAI", + "AsyncWriterAI", + "Client", + "AsyncClient", +] + + +class WriterAI(SyncAPIClient): + chat: resources.ChatResource + completions: resources.CompletionsResource + models: resources.ModelsResource + with_raw_response: WriterAIWithRawResponse + with_streaming_response: WriterAIWithStreamedResponse + + # client options + api_key: str + + def __init__( + self, + *, + api_key: str | None = None, + base_url: str | httpx.URL | None = None, + timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN, + max_retries: int = DEFAULT_MAX_RETRIES, + default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + # Configure a custom httpx client. + # We provide a `DefaultHttpxClient` class that you can pass to retain the default values we use for `limits`, `timeout` & `follow_redirects`. + # See the [httpx documentation](https://www.python-httpx.org/api/#client) for more details. + http_client: httpx.Client | None = None, + # Enable or disable schema validation for data returned by the API. + # When enabled an error APIResponseValidationError is raised + # if the API responds with invalid data for the expected schema. + # + # This parameter may be removed or changed in the future. + # If you rely on this feature, please open a GitHub issue + # outlining your use-case to help us decide if it should be + # part of our public interface in the future. + _strict_response_validation: bool = False, + ) -> None: + """Construct a new synchronous writerai client instance. + + This automatically infers the `api_key` argument from the `WRITERAI_AUTH_TOKEN` environment variable if it is not provided. + """ + if api_key is None: + api_key = os.environ.get("WRITERAI_AUTH_TOKEN") + if api_key is None: + raise WriterAIError( + "The api_key client option must be set either by passing api_key to the client or by setting the WRITERAI_AUTH_TOKEN environment variable" + ) + self.api_key = api_key + + if base_url is None: + base_url = os.environ.get("WRITER_AI_BASE_URL") + if base_url is None: + base_url = f"https://api.qordobadev.com" + + super().__init__( + version=__version__, + base_url=base_url, + max_retries=max_retries, + timeout=timeout, + http_client=http_client, + custom_headers=default_headers, + custom_query=default_query, + _strict_response_validation=_strict_response_validation, + ) + + self._default_stream_cls = Stream + + self.chat = resources.ChatResource(self) + self.completions = resources.CompletionsResource(self) + self.models = resources.ModelsResource(self) + self.with_raw_response = WriterAIWithRawResponse(self) + self.with_streaming_response = WriterAIWithStreamedResponse(self) + + @property + @override + def qs(self) -> Querystring: + return Querystring(array_format="comma") + + @property + @override + def auth_headers(self) -> dict[str, str]: + api_key = self.api_key + return {"Authorization": f"Bearer {api_key}"} + + @property + @override + def default_headers(self) -> dict[str, str | Omit]: + return { + **super().default_headers, + "X-Stainless-Async": "false", + **self._custom_headers, + } + + def copy( + self, + *, + api_key: str | None = None, + base_url: str | httpx.URL | None = None, + timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + http_client: httpx.Client | None = None, + max_retries: int | NotGiven = NOT_GIVEN, + default_headers: Mapping[str, str] | None = None, + set_default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + set_default_query: Mapping[str, object] | None = None, + _extra_kwargs: Mapping[str, Any] = {}, + ) -> Self: + """ + Create a new client instance re-using the same options given to the current client with optional overriding. + """ + if default_headers is not None and set_default_headers is not None: + raise ValueError("The `default_headers` and `set_default_headers` arguments are mutually exclusive") + + if default_query is not None and set_default_query is not None: + raise ValueError("The `default_query` and `set_default_query` arguments are mutually exclusive") + + headers = self._custom_headers + if default_headers is not None: + headers = {**headers, **default_headers} + elif set_default_headers is not None: + headers = set_default_headers + + params = self._custom_query + if default_query is not None: + params = {**params, **default_query} + elif set_default_query is not None: + params = set_default_query + + http_client = http_client or self._client + return self.__class__( + api_key=api_key or self.api_key, + base_url=base_url or self.base_url, + timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, + http_client=http_client, + max_retries=max_retries if is_given(max_retries) else self.max_retries, + default_headers=headers, + default_query=params, + **_extra_kwargs, + ) + + # Alias for `copy` for nicer inline usage, e.g. + # client.with_options(timeout=10).foo.create(...) + with_options = copy + + @override + def _make_status_error( + self, + err_msg: str, + *, + body: object, + response: httpx.Response, + ) -> APIStatusError: + if response.status_code == 400: + return _exceptions.BadRequestError(err_msg, response=response, body=body) + + if response.status_code == 401: + return _exceptions.AuthenticationError(err_msg, response=response, body=body) + + if response.status_code == 403: + return _exceptions.PermissionDeniedError(err_msg, response=response, body=body) + + if response.status_code == 404: + return _exceptions.NotFoundError(err_msg, response=response, body=body) + + if response.status_code == 409: + return _exceptions.ConflictError(err_msg, response=response, body=body) + + if response.status_code == 422: + return _exceptions.UnprocessableEntityError(err_msg, response=response, body=body) + + if response.status_code == 429: + return _exceptions.RateLimitError(err_msg, response=response, body=body) + + if response.status_code >= 500: + return _exceptions.InternalServerError(err_msg, response=response, body=body) + return APIStatusError(err_msg, response=response, body=body) + + +class AsyncWriterAI(AsyncAPIClient): + chat: resources.AsyncChatResource + completions: resources.AsyncCompletionsResource + models: resources.AsyncModelsResource + with_raw_response: AsyncWriterAIWithRawResponse + with_streaming_response: AsyncWriterAIWithStreamedResponse + + # client options + api_key: str + + def __init__( + self, + *, + api_key: str | None = None, + base_url: str | httpx.URL | None = None, + timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN, + max_retries: int = DEFAULT_MAX_RETRIES, + default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + # Configure a custom httpx client. + # We provide a `DefaultAsyncHttpxClient` class that you can pass to retain the default values we use for `limits`, `timeout` & `follow_redirects`. + # See the [httpx documentation](https://www.python-httpx.org/api/#asyncclient) for more details. + http_client: httpx.AsyncClient | None = None, + # Enable or disable schema validation for data returned by the API. + # When enabled an error APIResponseValidationError is raised + # if the API responds with invalid data for the expected schema. + # + # This parameter may be removed or changed in the future. + # If you rely on this feature, please open a GitHub issue + # outlining your use-case to help us decide if it should be + # part of our public interface in the future. + _strict_response_validation: bool = False, + ) -> None: + """Construct a new async writerai client instance. + + This automatically infers the `api_key` argument from the `WRITERAI_AUTH_TOKEN` environment variable if it is not provided. + """ + if api_key is None: + api_key = os.environ.get("WRITERAI_AUTH_TOKEN") + if api_key is None: + raise WriterAIError( + "The api_key client option must be set either by passing api_key to the client or by setting the WRITERAI_AUTH_TOKEN environment variable" + ) + self.api_key = api_key + + if base_url is None: + base_url = os.environ.get("WRITER_AI_BASE_URL") + if base_url is None: + base_url = f"https://api.qordobadev.com" + + super().__init__( + version=__version__, + base_url=base_url, + max_retries=max_retries, + timeout=timeout, + http_client=http_client, + custom_headers=default_headers, + custom_query=default_query, + _strict_response_validation=_strict_response_validation, + ) + + self._default_stream_cls = AsyncStream + + self.chat = resources.AsyncChatResource(self) + self.completions = resources.AsyncCompletionsResource(self) + self.models = resources.AsyncModelsResource(self) + self.with_raw_response = AsyncWriterAIWithRawResponse(self) + self.with_streaming_response = AsyncWriterAIWithStreamedResponse(self) + + @property + @override + def qs(self) -> Querystring: + return Querystring(array_format="comma") + + @property + @override + def auth_headers(self) -> dict[str, str]: + api_key = self.api_key + return {"Authorization": f"Bearer {api_key}"} + + @property + @override + def default_headers(self) -> dict[str, str | Omit]: + return { + **super().default_headers, + "X-Stainless-Async": f"async:{get_async_library()}", + **self._custom_headers, + } + + def copy( + self, + *, + api_key: str | None = None, + base_url: str | httpx.URL | None = None, + timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + http_client: httpx.AsyncClient | None = None, + max_retries: int | NotGiven = NOT_GIVEN, + default_headers: Mapping[str, str] | None = None, + set_default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + set_default_query: Mapping[str, object] | None = None, + _extra_kwargs: Mapping[str, Any] = {}, + ) -> Self: + """ + Create a new client instance re-using the same options given to the current client with optional overriding. + """ + if default_headers is not None and set_default_headers is not None: + raise ValueError("The `default_headers` and `set_default_headers` arguments are mutually exclusive") + + if default_query is not None and set_default_query is not None: + raise ValueError("The `default_query` and `set_default_query` arguments are mutually exclusive") + + headers = self._custom_headers + if default_headers is not None: + headers = {**headers, **default_headers} + elif set_default_headers is not None: + headers = set_default_headers + + params = self._custom_query + if default_query is not None: + params = {**params, **default_query} + elif set_default_query is not None: + params = set_default_query + + http_client = http_client or self._client + return self.__class__( + api_key=api_key or self.api_key, + base_url=base_url or self.base_url, + timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, + http_client=http_client, + max_retries=max_retries if is_given(max_retries) else self.max_retries, + default_headers=headers, + default_query=params, + **_extra_kwargs, + ) + + # Alias for `copy` for nicer inline usage, e.g. + # client.with_options(timeout=10).foo.create(...) + with_options = copy + + @override + def _make_status_error( + self, + err_msg: str, + *, + body: object, + response: httpx.Response, + ) -> APIStatusError: + if response.status_code == 400: + return _exceptions.BadRequestError(err_msg, response=response, body=body) + + if response.status_code == 401: + return _exceptions.AuthenticationError(err_msg, response=response, body=body) + + if response.status_code == 403: + return _exceptions.PermissionDeniedError(err_msg, response=response, body=body) + + if response.status_code == 404: + return _exceptions.NotFoundError(err_msg, response=response, body=body) + + if response.status_code == 409: + return _exceptions.ConflictError(err_msg, response=response, body=body) + + if response.status_code == 422: + return _exceptions.UnprocessableEntityError(err_msg, response=response, body=body) + + if response.status_code == 429: + return _exceptions.RateLimitError(err_msg, response=response, body=body) + + if response.status_code >= 500: + return _exceptions.InternalServerError(err_msg, response=response, body=body) + return APIStatusError(err_msg, response=response, body=body) + + +class WriterAIWithRawResponse: + def __init__(self, client: WriterAI) -> None: + self.chat = resources.ChatResourceWithRawResponse(client.chat) + self.completions = resources.CompletionsResourceWithRawResponse(client.completions) + self.models = resources.ModelsResourceWithRawResponse(client.models) + + +class AsyncWriterAIWithRawResponse: + def __init__(self, client: AsyncWriterAI) -> None: + self.chat = resources.AsyncChatResourceWithRawResponse(client.chat) + self.completions = resources.AsyncCompletionsResourceWithRawResponse(client.completions) + self.models = resources.AsyncModelsResourceWithRawResponse(client.models) + + +class WriterAIWithStreamedResponse: + def __init__(self, client: WriterAI) -> None: + self.chat = resources.ChatResourceWithStreamingResponse(client.chat) + self.completions = resources.CompletionsResourceWithStreamingResponse(client.completions) + self.models = resources.ModelsResourceWithStreamingResponse(client.models) + + +class AsyncWriterAIWithStreamedResponse: + def __init__(self, client: AsyncWriterAI) -> None: + self.chat = resources.AsyncChatResourceWithStreamingResponse(client.chat) + self.completions = resources.AsyncCompletionsResourceWithStreamingResponse(client.completions) + self.models = resources.AsyncModelsResourceWithStreamingResponse(client.models) + + +Client = WriterAI + +AsyncClient = AsyncWriterAI diff --git a/src/writer_ai/_compat.py b/src/writer_ai/_compat.py new file mode 100644 index 00000000..74c7639b --- /dev/null +++ b/src/writer_ai/_compat.py @@ -0,0 +1,222 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload +from datetime import date, datetime +from typing_extensions import Self + +import pydantic +from pydantic.fields import FieldInfo + +from ._types import StrBytesIntFloat + +_T = TypeVar("_T") +_ModelT = TypeVar("_ModelT", bound=pydantic.BaseModel) + +# --------------- Pydantic v2 compatibility --------------- + +# Pyright incorrectly reports some of our functions as overriding a method when they don't +# pyright: reportIncompatibleMethodOverride=false + +PYDANTIC_V2 = pydantic.VERSION.startswith("2.") + +# v1 re-exports +if TYPE_CHECKING: + + def parse_date(value: date | StrBytesIntFloat) -> date: # noqa: ARG001 + ... + + def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime: # noqa: ARG001 + ... + + def get_args(t: type[Any]) -> tuple[Any, ...]: # noqa: ARG001 + ... + + def is_union(tp: type[Any] | None) -> bool: # noqa: ARG001 + ... + + def get_origin(t: type[Any]) -> type[Any] | None: # noqa: ARG001 + ... + + def is_literal_type(type_: type[Any]) -> bool: # noqa: ARG001 + ... + + def is_typeddict(type_: type[Any]) -> bool: # noqa: ARG001 + ... + +else: + if PYDANTIC_V2: + from pydantic.v1.typing import ( + get_args as get_args, + is_union as is_union, + get_origin as get_origin, + is_typeddict as is_typeddict, + is_literal_type as is_literal_type, + ) + from pydantic.v1.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime + else: + from pydantic.typing import ( + get_args as get_args, + is_union as is_union, + get_origin as get_origin, + is_typeddict as is_typeddict, + is_literal_type as is_literal_type, + ) + from pydantic.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime + + +# refactored config +if TYPE_CHECKING: + from pydantic import ConfigDict as ConfigDict +else: + if PYDANTIC_V2: + from pydantic import ConfigDict + else: + # TODO: provide an error message here? + ConfigDict = None + + +# renamed methods / properties +def parse_obj(model: type[_ModelT], value: object) -> _ModelT: + if PYDANTIC_V2: + return model.model_validate(value) + else: + return cast(_ModelT, model.parse_obj(value)) # pyright: ignore[reportDeprecated, reportUnnecessaryCast] + + +def field_is_required(field: FieldInfo) -> bool: + if PYDANTIC_V2: + return field.is_required() + return field.required # type: ignore + + +def field_get_default(field: FieldInfo) -> Any: + value = field.get_default() + if PYDANTIC_V2: + from pydantic_core import PydanticUndefined + + if value == PydanticUndefined: + return None + return value + return value + + +def field_outer_type(field: FieldInfo) -> Any: + if PYDANTIC_V2: + return field.annotation + return field.outer_type_ # type: ignore + + +def get_model_config(model: type[pydantic.BaseModel]) -> Any: + if PYDANTIC_V2: + return model.model_config + return model.__config__ # type: ignore + + +def get_model_fields(model: type[pydantic.BaseModel]) -> dict[str, FieldInfo]: + if PYDANTIC_V2: + return model.model_fields + return model.__fields__ # type: ignore + + +def model_copy(model: _ModelT) -> _ModelT: + if PYDANTIC_V2: + return model.model_copy() + return model.copy() # type: ignore + + +def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str: + if PYDANTIC_V2: + return model.model_dump_json(indent=indent) + return model.json(indent=indent) # type: ignore + + +def model_dump( + model: pydantic.BaseModel, + *, + exclude_unset: bool = False, + exclude_defaults: bool = False, +) -> dict[str, Any]: + if PYDANTIC_V2: + return model.model_dump( + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + ) + return cast( + "dict[str, Any]", + model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast] + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + ), + ) + + +def model_parse(model: type[_ModelT], data: Any) -> _ModelT: + if PYDANTIC_V2: + return model.model_validate(data) + return model.parse_obj(data) # pyright: ignore[reportDeprecated] + + +# generic models +if TYPE_CHECKING: + + class GenericModel(pydantic.BaseModel): + ... + +else: + if PYDANTIC_V2: + # there no longer needs to be a distinction in v2 but + # we still have to create our own subclass to avoid + # inconsistent MRO ordering errors + class GenericModel(pydantic.BaseModel): + ... + + else: + import pydantic.generics + + class GenericModel(pydantic.generics.GenericModel, pydantic.BaseModel): + ... + + +# cached properties +if TYPE_CHECKING: + cached_property = property + + # we define a separate type (copied from typeshed) + # that represents that `cached_property` is `set`able + # at runtime, which differs from `@property`. + # + # this is a separate type as editors likely special case + # `@property` and we don't want to cause issues just to have + # more helpful internal types. + + class typed_cached_property(Generic[_T]): + func: Callable[[Any], _T] + attrname: str | None + + def __init__(self, func: Callable[[Any], _T]) -> None: + ... + + @overload + def __get__(self, instance: None, owner: type[Any] | None = None) -> Self: + ... + + @overload + def __get__(self, instance: object, owner: type[Any] | None = None) -> _T: + ... + + def __get__(self, instance: object, owner: type[Any] | None = None) -> _T | Self: + raise NotImplementedError() + + def __set_name__(self, owner: type[Any], name: str) -> None: + ... + + # __set__ is not defined at runtime, but @cached_property is designed to be settable + def __set__(self, instance: object, value: _T) -> None: + ... +else: + try: + from functools import cached_property as cached_property + except ImportError: + from cached_property import cached_property as cached_property + + typed_cached_property = cached_property diff --git a/src/writer_ai/_constants.py b/src/writer_ai/_constants.py new file mode 100644 index 00000000..a2ac3b6f --- /dev/null +++ b/src/writer_ai/_constants.py @@ -0,0 +1,14 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import httpx + +RAW_RESPONSE_HEADER = "X-Stainless-Raw-Response" +OVERRIDE_CAST_TO_HEADER = "____stainless_override_cast_to" + +# default timeout is 1 minute +DEFAULT_TIMEOUT = httpx.Timeout(timeout=60.0, connect=5.0) +DEFAULT_MAX_RETRIES = 2 +DEFAULT_CONNECTION_LIMITS = httpx.Limits(max_connections=100, max_keepalive_connections=20) + +INITIAL_RETRY_DELAY = 0.5 +MAX_RETRY_DELAY = 8.0 diff --git a/src/writer_ai/_exceptions.py b/src/writer_ai/_exceptions.py new file mode 100644 index 00000000..2ceb96a7 --- /dev/null +++ b/src/writer_ai/_exceptions.py @@ -0,0 +1,108 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal + +import httpx + +__all__ = [ + "BadRequestError", + "AuthenticationError", + "PermissionDeniedError", + "NotFoundError", + "ConflictError", + "UnprocessableEntityError", + "RateLimitError", + "InternalServerError", +] + + +class WriterAIError(Exception): + pass + + +class APIError(WriterAIError): + message: str + request: httpx.Request + + body: object | None + """The API response body. + + If the API responded with a valid JSON structure then this property will be the + decoded result. + + If it isn't a valid JSON structure then this will be the raw response. + + If there was no response associated with this error then it will be `None`. + """ + + def __init__(self, message: str, request: httpx.Request, *, body: object | None) -> None: # noqa: ARG002 + super().__init__(message) + self.request = request + self.message = message + self.body = body + + +class APIResponseValidationError(APIError): + response: httpx.Response + status_code: int + + def __init__(self, response: httpx.Response, body: object | None, *, message: str | None = None) -> None: + super().__init__(message or "Data returned by API invalid for expected schema.", response.request, body=body) + self.response = response + self.status_code = response.status_code + + +class APIStatusError(APIError): + """Raised when an API response has a status code of 4xx or 5xx.""" + + response: httpx.Response + status_code: int + + def __init__(self, message: str, *, response: httpx.Response, body: object | None) -> None: + super().__init__(message, response.request, body=body) + self.response = response + self.status_code = response.status_code + + +class APIConnectionError(APIError): + def __init__(self, *, message: str = "Connection error.", request: httpx.Request) -> None: + super().__init__(message, request, body=None) + + +class APITimeoutError(APIConnectionError): + def __init__(self, request: httpx.Request) -> None: + super().__init__(message="Request timed out.", request=request) + + +class BadRequestError(APIStatusError): + status_code: Literal[400] = 400 # pyright: ignore[reportIncompatibleVariableOverride] + + +class AuthenticationError(APIStatusError): + status_code: Literal[401] = 401 # pyright: ignore[reportIncompatibleVariableOverride] + + +class PermissionDeniedError(APIStatusError): + status_code: Literal[403] = 403 # pyright: ignore[reportIncompatibleVariableOverride] + + +class NotFoundError(APIStatusError): + status_code: Literal[404] = 404 # pyright: ignore[reportIncompatibleVariableOverride] + + +class ConflictError(APIStatusError): + status_code: Literal[409] = 409 # pyright: ignore[reportIncompatibleVariableOverride] + + +class UnprocessableEntityError(APIStatusError): + status_code: Literal[422] = 422 # pyright: ignore[reportIncompatibleVariableOverride] + + +class RateLimitError(APIStatusError): + status_code: Literal[429] = 429 # pyright: ignore[reportIncompatibleVariableOverride] + + +class InternalServerError(APIStatusError): + pass diff --git a/src/writer_ai/_files.py b/src/writer_ai/_files.py new file mode 100644 index 00000000..0d2022ae --- /dev/null +++ b/src/writer_ai/_files.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +import io +import os +import pathlib +from typing import overload +from typing_extensions import TypeGuard + +import anyio + +from ._types import ( + FileTypes, + FileContent, + RequestFiles, + HttpxFileTypes, + Base64FileInput, + HttpxFileContent, + HttpxRequestFiles, +) +from ._utils import is_tuple_t, is_mapping_t, is_sequence_t + + +def is_base64_file_input(obj: object) -> TypeGuard[Base64FileInput]: + return isinstance(obj, io.IOBase) or isinstance(obj, os.PathLike) + + +def is_file_content(obj: object) -> TypeGuard[FileContent]: + return ( + isinstance(obj, bytes) or isinstance(obj, tuple) or isinstance(obj, io.IOBase) or isinstance(obj, os.PathLike) + ) + + +def assert_is_file_content(obj: object, *, key: str | None = None) -> None: + if not is_file_content(obj): + prefix = f"Expected entry at `{key}`" if key is not None else f"Expected file input `{obj!r}`" + raise RuntimeError( + f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead." + ) from None + + +@overload +def to_httpx_files(files: None) -> None: + ... + + +@overload +def to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: + ... + + +def to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles | None: + if files is None: + return None + + if is_mapping_t(files): + files = {key: _transform_file(file) for key, file in files.items()} + elif is_sequence_t(files): + files = [(key, _transform_file(file)) for key, file in files] + else: + raise TypeError(f"Unexpected file type input {type(files)}, expected mapping or sequence") + + return files + + +def _transform_file(file: FileTypes) -> HttpxFileTypes: + if is_file_content(file): + if isinstance(file, os.PathLike): + path = pathlib.Path(file) + return (path.name, path.read_bytes()) + + return file + + if is_tuple_t(file): + return (file[0], _read_file_content(file[1]), *file[2:]) + + raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple") + + +def _read_file_content(file: FileContent) -> HttpxFileContent: + if isinstance(file, os.PathLike): + return pathlib.Path(file).read_bytes() + return file + + +@overload +async def async_to_httpx_files(files: None) -> None: + ... + + +@overload +async def async_to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: + ... + + +async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles | None: + if files is None: + return None + + if is_mapping_t(files): + files = {key: await _async_transform_file(file) for key, file in files.items()} + elif is_sequence_t(files): + files = [(key, await _async_transform_file(file)) for key, file in files] + else: + raise TypeError("Unexpected file type input {type(files)}, expected mapping or sequence") + + return files + + +async def _async_transform_file(file: FileTypes) -> HttpxFileTypes: + if is_file_content(file): + if isinstance(file, os.PathLike): + path = anyio.Path(file) + return (path.name, await path.read_bytes()) + + return file + + if is_tuple_t(file): + return (file[0], await _async_read_file_content(file[1]), *file[2:]) + + raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple") + + +async def _async_read_file_content(file: FileContent) -> HttpxFileContent: + if isinstance(file, os.PathLike): + return await anyio.Path(file).read_bytes() + + return file diff --git a/src/writer_ai/_models.py b/src/writer_ai/_models.py new file mode 100644 index 00000000..ff3f54e2 --- /dev/null +++ b/src/writer_ai/_models.py @@ -0,0 +1,727 @@ +from __future__ import annotations + +import os +import inspect +from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, cast +from datetime import date, datetime +from typing_extensions import ( + Unpack, + Literal, + ClassVar, + Protocol, + Required, + TypedDict, + TypeGuard, + final, + override, + runtime_checkable, +) + +import pydantic +import pydantic.generics +from pydantic.fields import FieldInfo + +from ._types import ( + Body, + IncEx, + Query, + ModelT, + Headers, + Timeout, + NotGiven, + AnyMapping, + HttpxRequestFiles, +) +from ._utils import ( + PropertyInfo, + is_list, + is_given, + lru_cache, + is_mapping, + parse_date, + coerce_boolean, + parse_datetime, + strip_not_given, + extract_type_arg, + is_annotated_type, + strip_annotated_type, +) +from ._compat import ( + PYDANTIC_V2, + ConfigDict, + GenericModel as BaseGenericModel, + get_args, + is_union, + parse_obj, + get_origin, + is_literal_type, + get_model_config, + get_model_fields, + field_get_default, +) +from ._constants import RAW_RESPONSE_HEADER + +if TYPE_CHECKING: + from pydantic_core.core_schema import ModelField, ModelFieldsSchema + +__all__ = ["BaseModel", "GenericModel"] + +_T = TypeVar("_T") + + +@runtime_checkable +class _ConfigProtocol(Protocol): + allow_population_by_field_name: bool + + +class BaseModel(pydantic.BaseModel): + if PYDANTIC_V2: + model_config: ClassVar[ConfigDict] = ConfigDict( + extra="allow", defer_build=coerce_boolean(os.environ.get("DEFER_PYDANTIC_BUILD", "true")) + ) + else: + + @property + @override + def model_fields_set(self) -> set[str]: + # a forwards-compat shim for pydantic v2 + return self.__fields_set__ # type: ignore + + class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated] + extra: Any = pydantic.Extra.allow # type: ignore + + def to_dict( + self, + *, + mode: Literal["json", "python"] = "python", + use_api_names: bool = True, + exclude_unset: bool = True, + exclude_defaults: bool = False, + exclude_none: bool = False, + warnings: bool = True, + ) -> dict[str, object]: + """Recursively generate a dictionary representation of the model, optionally specifying which fields to include or exclude. + + By default, fields that were not set by the API will not be included, + and keys will match the API response, *not* the property names from the model. + + For example, if the API responds with `"fooBar": true` but we've defined a `foo_bar: bool` property, + the output will use the `"fooBar"` key (unless `use_api_names=False` is passed). + + Args: + mode: + If mode is 'json', the dictionary will only contain JSON serializable types. e.g. `datetime` will be turned into a string, `"2024-3-22T18:11:19.117000Z"`. + If mode is 'python', the dictionary may contain any Python objects. e.g. `datetime(2024, 3, 22)` + + use_api_names: Whether to use the key that the API responded with or the property name. Defaults to `True`. + exclude_unset: Whether to exclude fields that have not been explicitly set. + exclude_defaults: Whether to exclude fields that are set to their default value from the output. + exclude_none: Whether to exclude fields that have a value of `None` from the output. + warnings: Whether to log warnings when invalid fields are encountered. This is only supported in Pydantic v2. + """ + return self.model_dump( + mode=mode, + by_alias=use_api_names, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + warnings=warnings, + ) + + def to_json( + self, + *, + indent: int | None = 2, + use_api_names: bool = True, + exclude_unset: bool = True, + exclude_defaults: bool = False, + exclude_none: bool = False, + warnings: bool = True, + ) -> str: + """Generates a JSON string representing this model as it would be received from or sent to the API (but with indentation). + + By default, fields that were not set by the API will not be included, + and keys will match the API response, *not* the property names from the model. + + For example, if the API responds with `"fooBar": true` but we've defined a `foo_bar: bool` property, + the output will use the `"fooBar"` key (unless `use_api_names=False` is passed). + + Args: + indent: Indentation to use in the JSON output. If `None` is passed, the output will be compact. Defaults to `2` + use_api_names: Whether to use the key that the API responded with or the property name. Defaults to `True`. + exclude_unset: Whether to exclude fields that have not been explicitly set. + exclude_defaults: Whether to exclude fields that have the default value. + exclude_none: Whether to exclude fields that have a value of `None`. + warnings: Whether to show any warnings that occurred during serialization. This is only supported in Pydantic v2. + """ + return self.model_dump_json( + indent=indent, + by_alias=use_api_names, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + warnings=warnings, + ) + + @override + def __str__(self) -> str: + # mypy complains about an invalid self arg + return f'{self.__repr_name__()}({self.__repr_str__(", ")})' # type: ignore[misc] + + # Override the 'construct' method in a way that supports recursive parsing without validation. + # Based on https://github.com/samuelcolvin/pydantic/issues/1168#issuecomment-817742836. + @classmethod + @override + def construct( + cls: Type[ModelT], + _fields_set: set[str] | None = None, + **values: object, + ) -> ModelT: + m = cls.__new__(cls) + fields_values: dict[str, object] = {} + + config = get_model_config(cls) + populate_by_name = ( + config.allow_population_by_field_name + if isinstance(config, _ConfigProtocol) + else config.get("populate_by_name") + ) + + if _fields_set is None: + _fields_set = set() + + model_fields = get_model_fields(cls) + for name, field in model_fields.items(): + key = field.alias + if key is None or (key not in values and populate_by_name): + key = name + + if key in values: + fields_values[name] = _construct_field(value=values[key], field=field, key=key) + _fields_set.add(name) + else: + fields_values[name] = field_get_default(field) + + _extra = {} + for key, value in values.items(): + if key not in model_fields: + if PYDANTIC_V2: + _extra[key] = value + else: + _fields_set.add(key) + fields_values[key] = value + + object.__setattr__(m, "__dict__", fields_values) + + if PYDANTIC_V2: + # these properties are copied from Pydantic's `model_construct()` method + object.__setattr__(m, "__pydantic_private__", None) + object.__setattr__(m, "__pydantic_extra__", _extra) + object.__setattr__(m, "__pydantic_fields_set__", _fields_set) + else: + # init_private_attributes() does not exist in v2 + m._init_private_attributes() # type: ignore + + # copied from Pydantic v1's `construct()` method + object.__setattr__(m, "__fields_set__", _fields_set) + + return m + + if not TYPE_CHECKING: + # type checkers incorrectly complain about this assignment + # because the type signatures are technically different + # although not in practice + model_construct = construct + + if not PYDANTIC_V2: + # we define aliases for some of the new pydantic v2 methods so + # that we can just document these methods without having to specify + # a specific pydantic version as some users may not know which + # pydantic version they are currently using + + @override + def model_dump( + self, + *, + mode: Literal["json", "python"] | str = "python", + include: IncEx = None, + exclude: IncEx = None, + by_alias: bool = False, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + round_trip: bool = False, + warnings: bool = True, + ) -> dict[str, Any]: + """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump + + Generate a dictionary representation of the model, optionally specifying which fields to include or exclude. + + Args: + mode: The mode in which `to_python` should run. + If mode is 'json', the dictionary will only contain JSON serializable types. + If mode is 'python', the dictionary may contain any Python objects. + include: A list of fields to include in the output. + exclude: A list of fields to exclude from the output. + by_alias: Whether to use the field's alias in the dictionary key if defined. + exclude_unset: Whether to exclude fields that are unset or None from the output. + exclude_defaults: Whether to exclude fields that are set to their default value from the output. + exclude_none: Whether to exclude fields that have a value of `None` from the output. + round_trip: Whether to enable serialization and deserialization round-trip support. + warnings: Whether to log warnings when invalid fields are encountered. + + Returns: + A dictionary representation of the model. + """ + if mode != "python": + raise ValueError("mode is only supported in Pydantic v2") + if round_trip != False: + raise ValueError("round_trip is only supported in Pydantic v2") + if warnings != True: + raise ValueError("warnings is only supported in Pydantic v2") + return super().dict( # pyright: ignore[reportDeprecated] + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + + @override + def model_dump_json( + self, + *, + indent: int | None = None, + include: IncEx = None, + exclude: IncEx = None, + by_alias: bool = False, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + round_trip: bool = False, + warnings: bool = True, + ) -> str: + """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump_json + + Generates a JSON representation of the model using Pydantic's `to_json` method. + + Args: + indent: Indentation to use in the JSON output. If None is passed, the output will be compact. + include: Field(s) to include in the JSON output. Can take either a string or set of strings. + exclude: Field(s) to exclude from the JSON output. Can take either a string or set of strings. + by_alias: Whether to serialize using field aliases. + exclude_unset: Whether to exclude fields that have not been explicitly set. + exclude_defaults: Whether to exclude fields that have the default value. + exclude_none: Whether to exclude fields that have a value of `None`. + round_trip: Whether to use serialization/deserialization between JSON and class instance. + warnings: Whether to show any warnings that occurred during serialization. + + Returns: + A JSON string representation of the model. + """ + if round_trip != False: + raise ValueError("round_trip is only supported in Pydantic v2") + if warnings != True: + raise ValueError("warnings is only supported in Pydantic v2") + return super().json( # type: ignore[reportDeprecated] + indent=indent, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + + +def _construct_field(value: object, field: FieldInfo, key: str) -> object: + if value is None: + return field_get_default(field) + + if PYDANTIC_V2: + type_ = field.annotation + else: + type_ = cast(type, field.outer_type_) # type: ignore + + if type_ is None: + raise RuntimeError(f"Unexpected field type is None for {key}") + + return construct_type(value=value, type_=type_) + + +def is_basemodel(type_: type) -> bool: + """Returns whether or not the given type is either a `BaseModel` or a union of `BaseModel`""" + if is_union(type_): + for variant in get_args(type_): + if is_basemodel(variant): + return True + + return False + + return is_basemodel_type(type_) + + +def is_basemodel_type(type_: type) -> TypeGuard[type[BaseModel] | type[GenericModel]]: + origin = get_origin(type_) or type_ + return issubclass(origin, BaseModel) or issubclass(origin, GenericModel) + + +def construct_type(*, value: object, type_: object) -> object: + """Loose coercion to the expected type with construction of nested values. + + If the given value does not match the expected type then it is returned as-is. + """ + # we allow `object` as the input type because otherwise, passing things like + # `Literal['value']` will be reported as a type error by type checkers + type_ = cast("type[object]", type_) + + # unwrap `Annotated[T, ...]` -> `T` + if is_annotated_type(type_): + meta: tuple[Any, ...] = get_args(type_)[1:] + type_ = extract_type_arg(type_, 0) + else: + meta = tuple() + + # we need to use the origin class for any types that are subscripted generics + # e.g. Dict[str, object] + origin = get_origin(type_) or type_ + args = get_args(type_) + + if is_union(origin): + try: + return validate_type(type_=cast("type[object]", type_), value=value) + except Exception: + pass + + # if the type is a discriminated union then we want to construct the right variant + # in the union, even if the data doesn't match exactly, otherwise we'd break code + # that relies on the constructed class types, e.g. + # + # class FooType: + # kind: Literal['foo'] + # value: str + # + # class BarType: + # kind: Literal['bar'] + # value: int + # + # without this block, if the data we get is something like `{'kind': 'bar', 'value': 'foo'}` then + # we'd end up constructing `FooType` when it should be `BarType`. + discriminator = _build_discriminated_union_meta(union=type_, meta_annotations=meta) + if discriminator and is_mapping(value): + variant_value = value.get(discriminator.field_alias_from or discriminator.field_name) + if variant_value and isinstance(variant_value, str): + variant_type = discriminator.mapping.get(variant_value) + if variant_type: + return construct_type(type_=variant_type, value=value) + + # if the data is not valid, use the first variant that doesn't fail while deserializing + for variant in args: + try: + return construct_type(value=value, type_=variant) + except Exception: + continue + + raise RuntimeError(f"Could not convert data into a valid instance of {type_}") + + if origin == dict: + if not is_mapping(value): + return value + + _, items_type = get_args(type_) # Dict[_, items_type] + return {key: construct_type(value=item, type_=items_type) for key, item in value.items()} + + if not is_literal_type(type_) and (issubclass(origin, BaseModel) or issubclass(origin, GenericModel)): + if is_list(value): + return [cast(Any, type_).construct(**entry) if is_mapping(entry) else entry for entry in value] + + if is_mapping(value): + if issubclass(type_, BaseModel): + return type_.construct(**value) # type: ignore[arg-type] + + return cast(Any, type_).construct(**value) + + if origin == list: + if not is_list(value): + return value + + inner_type = args[0] # List[inner_type] + return [construct_type(value=entry, type_=inner_type) for entry in value] + + if origin == float: + if isinstance(value, int): + coerced = float(value) + if coerced != value: + return value + return coerced + + return value + + if type_ == datetime: + try: + return parse_datetime(value) # type: ignore + except Exception: + return value + + if type_ == date: + try: + return parse_date(value) # type: ignore + except Exception: + return value + + return value + + +@runtime_checkable +class CachedDiscriminatorType(Protocol): + __discriminator__: DiscriminatorDetails + + +class DiscriminatorDetails: + field_name: str + """The name of the discriminator field in the variant class, e.g. + + ```py + class Foo(BaseModel): + type: Literal['foo'] + ``` + + Will result in field_name='type' + """ + + field_alias_from: str | None + """The name of the discriminator field in the API response, e.g. + + ```py + class Foo(BaseModel): + type: Literal['foo'] = Field(alias='type_from_api') + ``` + + Will result in field_alias_from='type_from_api' + """ + + mapping: dict[str, type] + """Mapping of discriminator value to variant type, e.g. + + {'foo': FooVariant, 'bar': BarVariant} + """ + + def __init__( + self, + *, + mapping: dict[str, type], + discriminator_field: str, + discriminator_alias: str | None, + ) -> None: + self.mapping = mapping + self.field_name = discriminator_field + self.field_alias_from = discriminator_alias + + +def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, ...]) -> DiscriminatorDetails | None: + if isinstance(union, CachedDiscriminatorType): + return union.__discriminator__ + + discriminator_field_name: str | None = None + + for annotation in meta_annotations: + if isinstance(annotation, PropertyInfo) and annotation.discriminator is not None: + discriminator_field_name = annotation.discriminator + break + + if not discriminator_field_name: + return None + + mapping: dict[str, type] = {} + discriminator_alias: str | None = None + + for variant in get_args(union): + variant = strip_annotated_type(variant) + if is_basemodel_type(variant): + if PYDANTIC_V2: + field = _extract_field_schema_pv2(variant, discriminator_field_name) + if not field: + continue + + # Note: if one variant defines an alias then they all should + discriminator_alias = field.get("serialization_alias") + + field_schema = field["schema"] + + if field_schema["type"] == "literal": + for entry in field_schema["expected"]: + if isinstance(entry, str): + mapping[entry] = variant + else: + field_info = cast("dict[str, FieldInfo]", variant.__fields__).get(discriminator_field_name) # pyright: ignore[reportDeprecated, reportUnnecessaryCast] + if not field_info: + continue + + # Note: if one variant defines an alias then they all should + discriminator_alias = field_info.alias + + if field_info.annotation and is_literal_type(field_info.annotation): + for entry in get_args(field_info.annotation): + if isinstance(entry, str): + mapping[entry] = variant + + if not mapping: + return None + + details = DiscriminatorDetails( + mapping=mapping, + discriminator_field=discriminator_field_name, + discriminator_alias=discriminator_alias, + ) + cast(CachedDiscriminatorType, union).__discriminator__ = details + return details + + +def _extract_field_schema_pv2(model: type[BaseModel], field_name: str) -> ModelField | None: + schema = model.__pydantic_core_schema__ + if schema["type"] != "model": + return None + + fields_schema = schema["schema"] + if fields_schema["type"] != "model-fields": + return None + + fields_schema = cast("ModelFieldsSchema", fields_schema) + + field = fields_schema["fields"].get(field_name) + if not field: + return None + + return cast("ModelField", field) # pyright: ignore[reportUnnecessaryCast] + + +def validate_type(*, type_: type[_T], value: object) -> _T: + """Strict validation that the given value matches the expected type""" + if inspect.isclass(type_) and issubclass(type_, pydantic.BaseModel): + return cast(_T, parse_obj(type_, value)) + + return cast(_T, _validate_non_model_type(type_=type_, value=value)) + + +# our use of subclasssing here causes weirdness for type checkers, +# so we just pretend that we don't subclass +if TYPE_CHECKING: + GenericModel = BaseModel +else: + + class GenericModel(BaseGenericModel, BaseModel): + pass + + +if PYDANTIC_V2: + from pydantic import TypeAdapter as _TypeAdapter + + _CachedTypeAdapter = cast("TypeAdapter[object]", lru_cache(maxsize=None)(_TypeAdapter)) + + if TYPE_CHECKING: + from pydantic import TypeAdapter + else: + TypeAdapter = _CachedTypeAdapter + + def _validate_non_model_type(*, type_: type[_T], value: object) -> _T: + return TypeAdapter(type_).validate_python(value) + +elif not TYPE_CHECKING: # TODO: condition is weird + + class RootModel(GenericModel, Generic[_T]): + """Used as a placeholder to easily convert runtime types to a Pydantic format + to provide validation. + + For example: + ```py + validated = RootModel[int](__root__="5").__root__ + # validated: 5 + ``` + """ + + __root__: _T + + def _validate_non_model_type(*, type_: type[_T], value: object) -> _T: + model = _create_pydantic_model(type_).validate(value) + return cast(_T, model.__root__) + + def _create_pydantic_model(type_: _T) -> Type[RootModel[_T]]: + return RootModel[type_] # type: ignore + + +class FinalRequestOptionsInput(TypedDict, total=False): + method: Required[str] + url: Required[str] + params: Query + headers: Headers + max_retries: int + timeout: float | Timeout | None + files: HttpxRequestFiles | None + idempotency_key: str + json_data: Body + extra_json: AnyMapping + + +@final +class FinalRequestOptions(pydantic.BaseModel): + method: str + url: str + params: Query = {} + headers: Union[Headers, NotGiven] = NotGiven() + max_retries: Union[int, NotGiven] = NotGiven() + timeout: Union[float, Timeout, None, NotGiven] = NotGiven() + files: Union[HttpxRequestFiles, None] = None + idempotency_key: Union[str, None] = None + post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() + + # It should be noted that we cannot use `json` here as that would override + # a BaseModel method in an incompatible fashion. + json_data: Union[Body, None] = None + extra_json: Union[AnyMapping, None] = None + + if PYDANTIC_V2: + model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True) + else: + + class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated] + arbitrary_types_allowed: bool = True + + def get_max_retries(self, max_retries: int) -> int: + if isinstance(self.max_retries, NotGiven): + return max_retries + return self.max_retries + + def _strip_raw_response_header(self) -> None: + if not is_given(self.headers): + return + + if self.headers.get(RAW_RESPONSE_HEADER): + self.headers = {**self.headers} + self.headers.pop(RAW_RESPONSE_HEADER) + + # override the `construct` method so that we can run custom transformations. + # this is necessary as we don't want to do any actual runtime type checking + # (which means we can't use validators) but we do want to ensure that `NotGiven` + # values are not present + # + # type ignore required because we're adding explicit types to `**values` + @classmethod + def construct( # type: ignore + cls, + _fields_set: set[str] | None = None, + **values: Unpack[FinalRequestOptionsInput], + ) -> FinalRequestOptions: + kwargs: dict[str, Any] = { + # we unconditionally call `strip_not_given` on any value + # as it will just ignore any non-mapping types + key: strip_not_given(value) + for key, value in values.items() + } + if PYDANTIC_V2: + return super().model_construct(_fields_set, **kwargs) + return cast(FinalRequestOptions, super().construct(_fields_set, **kwargs)) # pyright: ignore[reportDeprecated] + + if not TYPE_CHECKING: + # type checkers incorrectly complain about this assignment + model_construct = construct diff --git a/src/writer_ai/_qs.py b/src/writer_ai/_qs.py new file mode 100644 index 00000000..274320ca --- /dev/null +++ b/src/writer_ai/_qs.py @@ -0,0 +1,150 @@ +from __future__ import annotations + +from typing import Any, List, Tuple, Union, Mapping, TypeVar +from urllib.parse import parse_qs, urlencode +from typing_extensions import Literal, get_args + +from ._types import NOT_GIVEN, NotGiven, NotGivenOr +from ._utils import flatten + +_T = TypeVar("_T") + + +ArrayFormat = Literal["comma", "repeat", "indices", "brackets"] +NestedFormat = Literal["dots", "brackets"] + +PrimitiveData = Union[str, int, float, bool, None] +# this should be Data = Union[PrimitiveData, "List[Data]", "Tuple[Data]", "Mapping[str, Data]"] +# https://github.com/microsoft/pyright/issues/3555 +Data = Union[PrimitiveData, List[Any], Tuple[Any], "Mapping[str, Any]"] +Params = Mapping[str, Data] + + +class Querystring: + array_format: ArrayFormat + nested_format: NestedFormat + + def __init__( + self, + *, + array_format: ArrayFormat = "repeat", + nested_format: NestedFormat = "brackets", + ) -> None: + self.array_format = array_format + self.nested_format = nested_format + + def parse(self, query: str) -> Mapping[str, object]: + # Note: custom format syntax is not supported yet + return parse_qs(query) + + def stringify( + self, + params: Params, + *, + array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN, + nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN, + ) -> str: + return urlencode( + self.stringify_items( + params, + array_format=array_format, + nested_format=nested_format, + ) + ) + + def stringify_items( + self, + params: Params, + *, + array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN, + nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN, + ) -> list[tuple[str, str]]: + opts = Options( + qs=self, + array_format=array_format, + nested_format=nested_format, + ) + return flatten([self._stringify_item(key, value, opts) for key, value in params.items()]) + + def _stringify_item( + self, + key: str, + value: Data, + opts: Options, + ) -> list[tuple[str, str]]: + if isinstance(value, Mapping): + items: list[tuple[str, str]] = [] + nested_format = opts.nested_format + for subkey, subvalue in value.items(): + items.extend( + self._stringify_item( + # TODO: error if unknown format + f"{key}.{subkey}" if nested_format == "dots" else f"{key}[{subkey}]", + subvalue, + opts, + ) + ) + return items + + if isinstance(value, (list, tuple)): + array_format = opts.array_format + if array_format == "comma": + return [ + ( + key, + ",".join(self._primitive_value_to_str(item) for item in value if item is not None), + ), + ] + elif array_format == "repeat": + items = [] + for item in value: + items.extend(self._stringify_item(key, item, opts)) + return items + elif array_format == "indices": + raise NotImplementedError("The array indices format is not supported yet") + elif array_format == "brackets": + items = [] + key = key + "[]" + for item in value: + items.extend(self._stringify_item(key, item, opts)) + return items + else: + raise NotImplementedError( + f"Unknown array_format value: {array_format}, choose from {', '.join(get_args(ArrayFormat))}" + ) + + serialised = self._primitive_value_to_str(value) + if not serialised: + return [] + return [(key, serialised)] + + def _primitive_value_to_str(self, value: PrimitiveData) -> str: + # copied from httpx + if value is True: + return "true" + elif value is False: + return "false" + elif value is None: + return "" + return str(value) + + +_qs = Querystring() +parse = _qs.parse +stringify = _qs.stringify +stringify_items = _qs.stringify_items + + +class Options: + array_format: ArrayFormat + nested_format: NestedFormat + + def __init__( + self, + qs: Querystring = _qs, + *, + array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN, + nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN, + ) -> None: + self.array_format = qs.array_format if isinstance(array_format, NotGiven) else array_format + self.nested_format = qs.nested_format if isinstance(nested_format, NotGiven) else nested_format diff --git a/src/writer_ai/_resource.py b/src/writer_ai/_resource.py new file mode 100644 index 00000000..bc6c2634 --- /dev/null +++ b/src/writer_ai/_resource.py @@ -0,0 +1,43 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import time +from typing import TYPE_CHECKING + +import anyio + +if TYPE_CHECKING: + from ._client import WriterAI, AsyncWriterAI + + +class SyncAPIResource: + _client: WriterAI + + def __init__(self, client: WriterAI) -> None: + self._client = client + self._get = client.get + self._post = client.post + self._patch = client.patch + self._put = client.put + self._delete = client.delete + self._get_api_list = client.get_api_list + + def _sleep(self, seconds: float) -> None: + time.sleep(seconds) + + +class AsyncAPIResource: + _client: AsyncWriterAI + + def __init__(self, client: AsyncWriterAI) -> None: + self._client = client + self._get = client.get + self._post = client.post + self._patch = client.patch + self._put = client.put + self._delete = client.delete + self._get_api_list = client.get_api_list + + async def _sleep(self, seconds: float) -> None: + await anyio.sleep(seconds) diff --git a/src/writer_ai/_response.py b/src/writer_ai/_response.py new file mode 100644 index 00000000..98e579f9 --- /dev/null +++ b/src/writer_ai/_response.py @@ -0,0 +1,820 @@ +from __future__ import annotations + +import os +import inspect +import logging +import datetime +import functools +from types import TracebackType +from typing import ( + TYPE_CHECKING, + Any, + Union, + Generic, + TypeVar, + Callable, + Iterator, + AsyncIterator, + cast, + overload, +) +from typing_extensions import Awaitable, ParamSpec, override, get_origin + +import anyio +import httpx +import pydantic + +from ._types import NoneType +from ._utils import is_given, extract_type_arg, is_annotated_type, extract_type_var_from_base +from ._models import BaseModel, is_basemodel +from ._constants import RAW_RESPONSE_HEADER, OVERRIDE_CAST_TO_HEADER +from ._streaming import Stream, AsyncStream, is_stream_class_type, extract_stream_chunk_type +from ._exceptions import WriterAIError, APIResponseValidationError + +if TYPE_CHECKING: + from ._models import FinalRequestOptions + from ._base_client import BaseClient + + +P = ParamSpec("P") +R = TypeVar("R") +_T = TypeVar("_T") +_APIResponseT = TypeVar("_APIResponseT", bound="APIResponse[Any]") +_AsyncAPIResponseT = TypeVar("_AsyncAPIResponseT", bound="AsyncAPIResponse[Any]") + +log: logging.Logger = logging.getLogger(__name__) + + +class BaseAPIResponse(Generic[R]): + _cast_to: type[R] + _client: BaseClient[Any, Any] + _parsed_by_type: dict[type[Any], Any] + _is_sse_stream: bool + _stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None + _options: FinalRequestOptions + + http_response: httpx.Response + + def __init__( + self, + *, + raw: httpx.Response, + cast_to: type[R], + client: BaseClient[Any, Any], + stream: bool, + stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, + options: FinalRequestOptions, + ) -> None: + self._cast_to = cast_to + self._client = client + self._parsed_by_type = {} + self._is_sse_stream = stream + self._stream_cls = stream_cls + self._options = options + self.http_response = raw + + @property + def headers(self) -> httpx.Headers: + return self.http_response.headers + + @property + def http_request(self) -> httpx.Request: + """Returns the httpx Request instance associated with the current response.""" + return self.http_response.request + + @property + def status_code(self) -> int: + return self.http_response.status_code + + @property + def url(self) -> httpx.URL: + """Returns the URL for which the request was made.""" + return self.http_response.url + + @property + def method(self) -> str: + return self.http_request.method + + @property + def http_version(self) -> str: + return self.http_response.http_version + + @property + def elapsed(self) -> datetime.timedelta: + """The time taken for the complete request/response cycle to complete.""" + return self.http_response.elapsed + + @property + def is_closed(self) -> bool: + """Whether or not the response body has been closed. + + If this is False then there is response data that has not been read yet. + You must either fully consume the response body or call `.close()` + before discarding the response to prevent resource leaks. + """ + return self.http_response.is_closed + + @override + def __repr__(self) -> str: + return ( + f"<{self.__class__.__name__} [{self.status_code} {self.http_response.reason_phrase}] type={self._cast_to}>" + ) + + def _parse(self, *, to: type[_T] | None = None) -> R | _T: + # unwrap `Annotated[T, ...]` -> `T` + if to and is_annotated_type(to): + to = extract_type_arg(to, 0) + + if self._is_sse_stream: + if to: + if not is_stream_class_type(to): + raise TypeError(f"Expected custom parse type to be a subclass of {Stream} or {AsyncStream}") + + return cast( + _T, + to( + cast_to=extract_stream_chunk_type( + to, + failure_message="Expected custom stream type to be passed with a type argument, e.g. Stream[ChunkType]", + ), + response=self.http_response, + client=cast(Any, self._client), + ), + ) + + if self._stream_cls: + return cast( + R, + self._stream_cls( + cast_to=extract_stream_chunk_type(self._stream_cls), + response=self.http_response, + client=cast(Any, self._client), + ), + ) + + stream_cls = cast("type[Stream[Any]] | type[AsyncStream[Any]] | None", self._client._default_stream_cls) + if stream_cls is None: + raise MissingStreamClassError() + + return cast( + R, + stream_cls( + cast_to=self._cast_to, + response=self.http_response, + client=cast(Any, self._client), + ), + ) + + cast_to = to if to is not None else self._cast_to + + # unwrap `Annotated[T, ...]` -> `T` + if is_annotated_type(cast_to): + cast_to = extract_type_arg(cast_to, 0) + + if cast_to is NoneType: + return cast(R, None) + + response = self.http_response + if cast_to == str: + return cast(R, response.text) + + if cast_to == bytes: + return cast(R, response.content) + + if cast_to == int: + return cast(R, int(response.text)) + + if cast_to == float: + return cast(R, float(response.text)) + + origin = get_origin(cast_to) or cast_to + + if origin == APIResponse: + raise RuntimeError("Unexpected state - cast_to is `APIResponse`") + + if inspect.isclass(origin) and issubclass(origin, httpx.Response): + # Because of the invariance of our ResponseT TypeVar, users can subclass httpx.Response + # and pass that class to our request functions. We cannot change the variance to be either + # covariant or contravariant as that makes our usage of ResponseT illegal. We could construct + # the response class ourselves but that is something that should be supported directly in httpx + # as it would be easy to incorrectly construct the Response object due to the multitude of arguments. + if cast_to != httpx.Response: + raise ValueError(f"Subclasses of httpx.Response cannot be passed to `cast_to`") + return cast(R, response) + + if inspect.isclass(origin) and not issubclass(origin, BaseModel) and issubclass(origin, pydantic.BaseModel): + raise TypeError("Pydantic models must subclass our base model type, e.g. `from writer_ai import BaseModel`") + + if ( + cast_to is not object + and not origin is list + and not origin is dict + and not origin is Union + and not issubclass(origin, BaseModel) + ): + raise RuntimeError( + f"Unsupported type, expected {cast_to} to be a subclass of {BaseModel}, {dict}, {list}, {Union}, {NoneType}, {str} or {httpx.Response}." + ) + + # split is required to handle cases where additional information is included + # in the response, e.g. application/json; charset=utf-8 + content_type, *_ = response.headers.get("content-type", "*").split(";") + if content_type != "application/json": + if is_basemodel(cast_to): + try: + data = response.json() + except Exception as exc: + log.debug("Could not read JSON from response data due to %s - %s", type(exc), exc) + else: + return self._client._process_response_data( + data=data, + cast_to=cast_to, # type: ignore + response=response, + ) + + if self._client._strict_response_validation: + raise APIResponseValidationError( + response=response, + message=f"Expected Content-Type response header to be `application/json` but received `{content_type}` instead.", + body=response.text, + ) + + # If the API responds with content that isn't JSON then we just return + # the (decoded) text without performing any parsing so that you can still + # handle the response however you need to. + return response.text # type: ignore + + data = response.json() + + return self._client._process_response_data( + data=data, + cast_to=cast_to, # type: ignore + response=response, + ) + + +class APIResponse(BaseAPIResponse[R]): + @overload + def parse(self, *, to: type[_T]) -> _T: + ... + + @overload + def parse(self) -> R: + ... + + def parse(self, *, to: type[_T] | None = None) -> R | _T: + """Returns the rich python representation of this response's data. + + For lower-level control, see `.read()`, `.json()`, `.iter_bytes()`. + + You can customise the type that the response is parsed into through + the `to` argument, e.g. + + ```py + from writer_ai import BaseModel + + + class MyModel(BaseModel): + foo: str + + + obj = response.parse(to=MyModel) + print(obj.foo) + ``` + + We support parsing: + - `BaseModel` + - `dict` + - `list` + - `Union` + - `str` + - `int` + - `float` + - `httpx.Response` + """ + cache_key = to if to is not None else self._cast_to + cached = self._parsed_by_type.get(cache_key) + if cached is not None: + return cached # type: ignore[no-any-return] + + if not self._is_sse_stream: + self.read() + + parsed = self._parse(to=to) + if is_given(self._options.post_parser): + parsed = self._options.post_parser(parsed) + + self._parsed_by_type[cache_key] = parsed + return parsed + + def read(self) -> bytes: + """Read and return the binary response content.""" + try: + return self.http_response.read() + except httpx.StreamConsumed as exc: + # The default error raised by httpx isn't very + # helpful in our case so we re-raise it with + # a different error message. + raise StreamAlreadyConsumed() from exc + + def text(self) -> str: + """Read and decode the response content into a string.""" + self.read() + return self.http_response.text + + def json(self) -> object: + """Read and decode the JSON response content.""" + self.read() + return self.http_response.json() + + def close(self) -> None: + """Close the response and release the connection. + + Automatically called if the response body is read to completion. + """ + self.http_response.close() + + def iter_bytes(self, chunk_size: int | None = None) -> Iterator[bytes]: + """ + A byte-iterator over the decoded response content. + + This automatically handles gzip, deflate and brotli encoded responses. + """ + for chunk in self.http_response.iter_bytes(chunk_size): + yield chunk + + def iter_text(self, chunk_size: int | None = None) -> Iterator[str]: + """A str-iterator over the decoded response content + that handles both gzip, deflate, etc but also detects the content's + string encoding. + """ + for chunk in self.http_response.iter_text(chunk_size): + yield chunk + + def iter_lines(self) -> Iterator[str]: + """Like `iter_text()` but will only yield chunks for each line""" + for chunk in self.http_response.iter_lines(): + yield chunk + + +class AsyncAPIResponse(BaseAPIResponse[R]): + @overload + async def parse(self, *, to: type[_T]) -> _T: + ... + + @overload + async def parse(self) -> R: + ... + + async def parse(self, *, to: type[_T] | None = None) -> R | _T: + """Returns the rich python representation of this response's data. + + For lower-level control, see `.read()`, `.json()`, `.iter_bytes()`. + + You can customise the type that the response is parsed into through + the `to` argument, e.g. + + ```py + from writer_ai import BaseModel + + + class MyModel(BaseModel): + foo: str + + + obj = response.parse(to=MyModel) + print(obj.foo) + ``` + + We support parsing: + - `BaseModel` + - `dict` + - `list` + - `Union` + - `str` + - `httpx.Response` + """ + cache_key = to if to is not None else self._cast_to + cached = self._parsed_by_type.get(cache_key) + if cached is not None: + return cached # type: ignore[no-any-return] + + if not self._is_sse_stream: + await self.read() + + parsed = self._parse(to=to) + if is_given(self._options.post_parser): + parsed = self._options.post_parser(parsed) + + self._parsed_by_type[cache_key] = parsed + return parsed + + async def read(self) -> bytes: + """Read and return the binary response content.""" + try: + return await self.http_response.aread() + except httpx.StreamConsumed as exc: + # the default error raised by httpx isn't very + # helpful in our case so we re-raise it with + # a different error message + raise StreamAlreadyConsumed() from exc + + async def text(self) -> str: + """Read and decode the response content into a string.""" + await self.read() + return self.http_response.text + + async def json(self) -> object: + """Read and decode the JSON response content.""" + await self.read() + return self.http_response.json() + + async def close(self) -> None: + """Close the response and release the connection. + + Automatically called if the response body is read to completion. + """ + await self.http_response.aclose() + + async def iter_bytes(self, chunk_size: int | None = None) -> AsyncIterator[bytes]: + """ + A byte-iterator over the decoded response content. + + This automatically handles gzip, deflate and brotli encoded responses. + """ + async for chunk in self.http_response.aiter_bytes(chunk_size): + yield chunk + + async def iter_text(self, chunk_size: int | None = None) -> AsyncIterator[str]: + """A str-iterator over the decoded response content + that handles both gzip, deflate, etc but also detects the content's + string encoding. + """ + async for chunk in self.http_response.aiter_text(chunk_size): + yield chunk + + async def iter_lines(self) -> AsyncIterator[str]: + """Like `iter_text()` but will only yield chunks for each line""" + async for chunk in self.http_response.aiter_lines(): + yield chunk + + +class BinaryAPIResponse(APIResponse[bytes]): + """Subclass of APIResponse providing helpers for dealing with binary data. + + Note: If you want to stream the response data instead of eagerly reading it + all at once then you should use `.with_streaming_response` when making + the API request, e.g. `.with_streaming_response.get_binary_response()` + """ + + def write_to_file( + self, + file: str | os.PathLike[str], + ) -> None: + """Write the output to the given file. + + Accepts a filename or any path-like object, e.g. pathlib.Path + + Note: if you want to stream the data to the file instead of writing + all at once then you should use `.with_streaming_response` when making + the API request, e.g. `.with_streaming_response.get_binary_response()` + """ + with open(file, mode="wb") as f: + for data in self.iter_bytes(): + f.write(data) + + +class AsyncBinaryAPIResponse(AsyncAPIResponse[bytes]): + """Subclass of APIResponse providing helpers for dealing with binary data. + + Note: If you want to stream the response data instead of eagerly reading it + all at once then you should use `.with_streaming_response` when making + the API request, e.g. `.with_streaming_response.get_binary_response()` + """ + + async def write_to_file( + self, + file: str | os.PathLike[str], + ) -> None: + """Write the output to the given file. + + Accepts a filename or any path-like object, e.g. pathlib.Path + + Note: if you want to stream the data to the file instead of writing + all at once then you should use `.with_streaming_response` when making + the API request, e.g. `.with_streaming_response.get_binary_response()` + """ + path = anyio.Path(file) + async with await path.open(mode="wb") as f: + async for data in self.iter_bytes(): + await f.write(data) + + +class StreamedBinaryAPIResponse(APIResponse[bytes]): + def stream_to_file( + self, + file: str | os.PathLike[str], + *, + chunk_size: int | None = None, + ) -> None: + """Streams the output to the given file. + + Accepts a filename or any path-like object, e.g. pathlib.Path + """ + with open(file, mode="wb") as f: + for data in self.iter_bytes(chunk_size): + f.write(data) + + +class AsyncStreamedBinaryAPIResponse(AsyncAPIResponse[bytes]): + async def stream_to_file( + self, + file: str | os.PathLike[str], + *, + chunk_size: int | None = None, + ) -> None: + """Streams the output to the given file. + + Accepts a filename or any path-like object, e.g. pathlib.Path + """ + path = anyio.Path(file) + async with await path.open(mode="wb") as f: + async for data in self.iter_bytes(chunk_size): + await f.write(data) + + +class MissingStreamClassError(TypeError): + def __init__(self) -> None: + super().__init__( + "The `stream` argument was set to `True` but the `stream_cls` argument was not given. See `writer_ai._streaming` for reference", + ) + + +class StreamAlreadyConsumed(WriterAIError): + """ + Attempted to read or stream content, but the content has already + been streamed. + + This can happen if you use a method like `.iter_lines()` and then attempt + to read th entire response body afterwards, e.g. + + ```py + response = await client.post(...) + async for line in response.iter_lines(): + ... # do something with `line` + + content = await response.read() + # ^ error + ``` + + If you want this behaviour you'll need to either manually accumulate the response + content or call `await response.read()` before iterating over the stream. + """ + + def __init__(self) -> None: + message = ( + "Attempted to read or stream some content, but the content has " + "already been streamed. " + "This could be due to attempting to stream the response " + "content more than once." + "\n\n" + "You can fix this by manually accumulating the response content while streaming " + "or by calling `.read()` before starting to stream." + ) + super().__init__(message) + + +class ResponseContextManager(Generic[_APIResponseT]): + """Context manager for ensuring that a request is not made + until it is entered and that the response will always be closed + when the context manager exits + """ + + def __init__(self, request_func: Callable[[], _APIResponseT]) -> None: + self._request_func = request_func + self.__response: _APIResponseT | None = None + + def __enter__(self) -> _APIResponseT: + self.__response = self._request_func() + return self.__response + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + if self.__response is not None: + self.__response.close() + + +class AsyncResponseContextManager(Generic[_AsyncAPIResponseT]): + """Context manager for ensuring that a request is not made + until it is entered and that the response will always be closed + when the context manager exits + """ + + def __init__(self, api_request: Awaitable[_AsyncAPIResponseT]) -> None: + self._api_request = api_request + self.__response: _AsyncAPIResponseT | None = None + + async def __aenter__(self) -> _AsyncAPIResponseT: + self.__response = await self._api_request + return self.__response + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + if self.__response is not None: + await self.__response.close() + + +def to_streamed_response_wrapper(func: Callable[P, R]) -> Callable[P, ResponseContextManager[APIResponse[R]]]: + """Higher order function that takes one of our bound API methods and wraps it + to support streaming and returning the raw `APIResponse` object directly. + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> ResponseContextManager[APIResponse[R]]: + extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "stream" + + kwargs["extra_headers"] = extra_headers + + make_request = functools.partial(func, *args, **kwargs) + + return ResponseContextManager(cast(Callable[[], APIResponse[R]], make_request)) + + return wrapped + + +def async_to_streamed_response_wrapper( + func: Callable[P, Awaitable[R]], +) -> Callable[P, AsyncResponseContextManager[AsyncAPIResponse[R]]]: + """Higher order function that takes one of our bound API methods and wraps it + to support streaming and returning the raw `APIResponse` object directly. + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncResponseContextManager[AsyncAPIResponse[R]]: + extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "stream" + + kwargs["extra_headers"] = extra_headers + + make_request = func(*args, **kwargs) + + return AsyncResponseContextManager(cast(Awaitable[AsyncAPIResponse[R]], make_request)) + + return wrapped + + +def to_custom_streamed_response_wrapper( + func: Callable[P, object], + response_cls: type[_APIResponseT], +) -> Callable[P, ResponseContextManager[_APIResponseT]]: + """Higher order function that takes one of our bound API methods and an `APIResponse` class + and wraps the method to support streaming and returning the given response class directly. + + Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])` + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> ResponseContextManager[_APIResponseT]: + extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "stream" + extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls + + kwargs["extra_headers"] = extra_headers + + make_request = functools.partial(func, *args, **kwargs) + + return ResponseContextManager(cast(Callable[[], _APIResponseT], make_request)) + + return wrapped + + +def async_to_custom_streamed_response_wrapper( + func: Callable[P, Awaitable[object]], + response_cls: type[_AsyncAPIResponseT], +) -> Callable[P, AsyncResponseContextManager[_AsyncAPIResponseT]]: + """Higher order function that takes one of our bound API methods and an `APIResponse` class + and wraps the method to support streaming and returning the given response class directly. + + Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])` + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncResponseContextManager[_AsyncAPIResponseT]: + extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "stream" + extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls + + kwargs["extra_headers"] = extra_headers + + make_request = func(*args, **kwargs) + + return AsyncResponseContextManager(cast(Awaitable[_AsyncAPIResponseT], make_request)) + + return wrapped + + +def to_raw_response_wrapper(func: Callable[P, R]) -> Callable[P, APIResponse[R]]: + """Higher order function that takes one of our bound API methods and wraps it + to support returning the raw `APIResponse` object directly. + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> APIResponse[R]: + extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "raw" + + kwargs["extra_headers"] = extra_headers + + return cast(APIResponse[R], func(*args, **kwargs)) + + return wrapped + + +def async_to_raw_response_wrapper(func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[AsyncAPIResponse[R]]]: + """Higher order function that takes one of our bound API methods and wraps it + to support returning the raw `APIResponse` object directly. + """ + + @functools.wraps(func) + async def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncAPIResponse[R]: + extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "raw" + + kwargs["extra_headers"] = extra_headers + + return cast(AsyncAPIResponse[R], await func(*args, **kwargs)) + + return wrapped + + +def to_custom_raw_response_wrapper( + func: Callable[P, object], + response_cls: type[_APIResponseT], +) -> Callable[P, _APIResponseT]: + """Higher order function that takes one of our bound API methods and an `APIResponse` class + and wraps the method to support returning the given response class directly. + + Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])` + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> _APIResponseT: + extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "raw" + extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls + + kwargs["extra_headers"] = extra_headers + + return cast(_APIResponseT, func(*args, **kwargs)) + + return wrapped + + +def async_to_custom_raw_response_wrapper( + func: Callable[P, Awaitable[object]], + response_cls: type[_AsyncAPIResponseT], +) -> Callable[P, Awaitable[_AsyncAPIResponseT]]: + """Higher order function that takes one of our bound API methods and an `APIResponse` class + and wraps the method to support returning the given response class directly. + + Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])` + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> Awaitable[_AsyncAPIResponseT]: + extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "raw" + extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls + + kwargs["extra_headers"] = extra_headers + + return cast(Awaitable[_AsyncAPIResponseT], func(*args, **kwargs)) + + return wrapped + + +def extract_response_type(typ: type[BaseAPIResponse[Any]]) -> type: + """Given a type like `APIResponse[T]`, returns the generic type variable `T`. + + This also handles the case where a concrete subclass is given, e.g. + ```py + class MyResponse(APIResponse[bytes]): + ... + + extract_response_type(MyResponse) -> bytes + ``` + """ + return extract_type_var_from_base( + typ, + generic_bases=cast("tuple[type, ...]", (BaseAPIResponse, APIResponse, AsyncAPIResponse)), + index=0, + ) diff --git a/src/writer_ai/_streaming.py b/src/writer_ai/_streaming.py new file mode 100644 index 00000000..a3c6f7d7 --- /dev/null +++ b/src/writer_ai/_streaming.py @@ -0,0 +1,365 @@ +# Note: initially copied from https://github.com/florimondmanca/httpx-sse/blob/master/src/httpx_sse/_decoders.py +from __future__ import annotations + +import json +import inspect +from types import TracebackType +from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, AsyncIterator, cast +from typing_extensions import Self, Protocol, TypeGuard, override, get_origin, runtime_checkable + +import httpx + +from ._utils import extract_type_var_from_base + +if TYPE_CHECKING: + from ._client import WriterAI, AsyncWriterAI + + +_T = TypeVar("_T") + + +class Stream(Generic[_T]): + """Provides the core interface to iterate over a synchronous stream response.""" + + response: httpx.Response + + _decoder: SSEBytesDecoder + + def __init__( + self, + *, + cast_to: type[_T], + response: httpx.Response, + client: WriterAI, + ) -> None: + self.response = response + self._cast_to = cast_to + self._client = client + self._decoder = client._make_sse_decoder() + self._iterator = self.__stream__() + + def __next__(self) -> _T: + return self._iterator.__next__() + + def __iter__(self) -> Iterator[_T]: + for item in self._iterator: + yield item + + def _iter_events(self) -> Iterator[ServerSentEvent]: + yield from self._decoder.iter_bytes(self.response.iter_bytes()) + + def __stream__(self) -> Iterator[_T]: + cast_to = cast(Any, self._cast_to) + response = self.response + process_data = self._client._process_response_data + iterator = self._iter_events() + + for sse in iterator: + if sse.event is None: + yield process_data(data=sse.json(), cast_to=cast_to, response=response) + + if sse.event == "error": + body = sse.data + + try: + body = sse.json() + err_msg = f"{body}" + except Exception: + err_msg = sse.data or f"Error code: {response.status_code}" + + raise self._client._make_status_error( + err_msg, + body=body, + response=self.response, + ) + + # Ensure the entire stream is consumed + for _sse in iterator: + ... + + def __enter__(self) -> Self: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.close() + + def close(self) -> None: + """ + Close the response and release the connection. + + Automatically called if the response body is read to completion. + """ + self.response.close() + + +class AsyncStream(Generic[_T]): + """Provides the core interface to iterate over an asynchronous stream response.""" + + response: httpx.Response + + _decoder: SSEDecoder | SSEBytesDecoder + + def __init__( + self, + *, + cast_to: type[_T], + response: httpx.Response, + client: AsyncWriterAI, + ) -> None: + self.response = response + self._cast_to = cast_to + self._client = client + self._decoder = client._make_sse_decoder() + self._iterator = self.__stream__() + + async def __anext__(self) -> _T: + return await self._iterator.__anext__() + + async def __aiter__(self) -> AsyncIterator[_T]: + async for item in self._iterator: + yield item + + async def _iter_events(self) -> AsyncIterator[ServerSentEvent]: + async for sse in self._decoder.aiter_bytes(self.response.aiter_bytes()): + yield sse + + async def __stream__(self) -> AsyncIterator[_T]: + cast_to = cast(Any, self._cast_to) + response = self.response + process_data = self._client._process_response_data + iterator = self._iter_events() + + async for sse in iterator: + if sse.event is None: + yield process_data(data=sse.json(), cast_to=cast_to, response=response) + + if sse.event == "error": + body = sse.data + + try: + body = sse.json() + err_msg = f"{body}" + except Exception: + err_msg = sse.data or f"Error code: {response.status_code}" + + raise self._client._make_status_error( + err_msg, + body=body, + response=self.response, + ) + + # Ensure the entire stream is consumed + async for _sse in iterator: + ... + + async def __aenter__(self) -> Self: + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + await self.close() + + async def close(self) -> None: + """ + Close the response and release the connection. + + Automatically called if the response body is read to completion. + """ + await self.response.aclose() + + +class ServerSentEvent: + def __init__( + self, + *, + event: str | None = None, + data: str | None = None, + id: str | None = None, + retry: int | None = None, + ) -> None: + if data is None: + data = "" + + self._id = id + self._data = data + self._event = event or None + self._retry = retry + + @property + def event(self) -> str | None: + return self._event + + @property + def id(self) -> str | None: + return self._id + + @property + def retry(self) -> int | None: + return self._retry + + @property + def data(self) -> str: + return self._data + + def json(self) -> Any: + return json.loads(self.data) + + @override + def __repr__(self) -> str: + return f"ServerSentEvent(event={self.event}, data={self.data}, id={self.id}, retry={self.retry})" + + +class SSEDecoder: + _data: list[str] + _event: str | None + _retry: int | None + _last_event_id: str | None + + def __init__(self) -> None: + self._event = None + self._data = [] + self._last_event_id = None + self._retry = None + + def iter_bytes(self, iterator: Iterator[bytes]) -> Iterator[ServerSentEvent]: + """Given an iterator that yields raw binary data, iterate over it & yield every event encountered""" + for chunk in self._iter_chunks(iterator): + # Split before decoding so splitlines() only uses \r and \n + for raw_line in chunk.splitlines(): + line = raw_line.decode("utf-8") + sse = self.decode(line) + if sse: + yield sse + + def _iter_chunks(self, iterator: Iterator[bytes]) -> Iterator[bytes]: + """Given an iterator that yields raw binary data, iterate over it and yield individual SSE chunks""" + data = b"" + for chunk in iterator: + for line in chunk.splitlines(keepends=True): + data += line + if data.endswith((b"\r\r", b"\n\n", b"\r\n\r\n")): + yield data + data = b"" + if data: + yield data + + async def aiter_bytes(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[ServerSentEvent]: + """Given an iterator that yields raw binary data, iterate over it & yield every event encountered""" + async for chunk in self._aiter_chunks(iterator): + # Split before decoding so splitlines() only uses \r and \n + for raw_line in chunk.splitlines(): + line = raw_line.decode("utf-8") + sse = self.decode(line) + if sse: + yield sse + + async def _aiter_chunks(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[bytes]: + """Given an iterator that yields raw binary data, iterate over it and yield individual SSE chunks""" + data = b"" + async for chunk in iterator: + for line in chunk.splitlines(keepends=True): + data += line + if data.endswith((b"\r\r", b"\n\n", b"\r\n\r\n")): + yield data + data = b"" + if data: + yield data + + def decode(self, line: str) -> ServerSentEvent | None: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = None + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None + + +@runtime_checkable +class SSEBytesDecoder(Protocol): + def iter_bytes(self, iterator: Iterator[bytes]) -> Iterator[ServerSentEvent]: + """Given an iterator that yields raw binary data, iterate over it & yield every event encountered""" + ... + + def aiter_bytes(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[ServerSentEvent]: + """Given an async iterator that yields raw binary data, iterate over it & yield every event encountered""" + ... + + +def is_stream_class_type(typ: type) -> TypeGuard[type[Stream[object]] | type[AsyncStream[object]]]: + """TypeGuard for determining whether or not the given type is a subclass of `Stream` / `AsyncStream`""" + origin = get_origin(typ) or typ + return inspect.isclass(origin) and issubclass(origin, (Stream, AsyncStream)) + + +def extract_stream_chunk_type( + stream_cls: type, + *, + failure_message: str | None = None, +) -> type: + """Given a type like `Stream[T]`, returns the generic type variable `T`. + + This also handles the case where a concrete subclass is given, e.g. + ```py + class MyStream(Stream[bytes]): + ... + + extract_stream_chunk_type(MyStream) -> bytes + ``` + """ + from ._base_client import Stream, AsyncStream + + return extract_type_var_from_base( + stream_cls, + index=0, + generic_bases=cast("tuple[type, ...]", (Stream, AsyncStream)), + failure_message=failure_message, + ) diff --git a/src/writer_ai/_types.py b/src/writer_ai/_types.py new file mode 100644 index 00000000..9281309d --- /dev/null +++ b/src/writer_ai/_types.py @@ -0,0 +1,220 @@ +from __future__ import annotations + +from os import PathLike +from typing import ( + IO, + TYPE_CHECKING, + Any, + Dict, + List, + Type, + Tuple, + Union, + Mapping, + TypeVar, + Callable, + Optional, + Sequence, +) +from typing_extensions import Literal, Protocol, TypeAlias, TypedDict, override, runtime_checkable + +import httpx +import pydantic +from httpx import URL, Proxy, Timeout, Response, BaseTransport, AsyncBaseTransport + +if TYPE_CHECKING: + from ._models import BaseModel + from ._response import APIResponse, AsyncAPIResponse + +Transport = BaseTransport +AsyncTransport = AsyncBaseTransport +Query = Mapping[str, object] +Body = object +AnyMapping = Mapping[str, object] +ModelT = TypeVar("ModelT", bound=pydantic.BaseModel) +_T = TypeVar("_T") + + +# Approximates httpx internal ProxiesTypes and RequestFiles types +# while adding support for `PathLike` instances +ProxiesDict = Dict["str | URL", Union[None, str, URL, Proxy]] +ProxiesTypes = Union[str, Proxy, ProxiesDict] +if TYPE_CHECKING: + Base64FileInput = Union[IO[bytes], PathLike[str]] + FileContent = Union[IO[bytes], bytes, PathLike[str]] +else: + Base64FileInput = Union[IO[bytes], PathLike] + FileContent = Union[IO[bytes], bytes, PathLike] # PathLike is not subscriptable in Python 3.8. +FileTypes = Union[ + # file (or bytes) + FileContent, + # (filename, file (or bytes)) + Tuple[Optional[str], FileContent], + # (filename, file (or bytes), content_type) + Tuple[Optional[str], FileContent, Optional[str]], + # (filename, file (or bytes), content_type, headers) + Tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]], +] +RequestFiles = Union[Mapping[str, FileTypes], Sequence[Tuple[str, FileTypes]]] + +# duplicate of the above but without our custom file support +HttpxFileContent = Union[IO[bytes], bytes] +HttpxFileTypes = Union[ + # file (or bytes) + HttpxFileContent, + # (filename, file (or bytes)) + Tuple[Optional[str], HttpxFileContent], + # (filename, file (or bytes), content_type) + Tuple[Optional[str], HttpxFileContent, Optional[str]], + # (filename, file (or bytes), content_type, headers) + Tuple[Optional[str], HttpxFileContent, Optional[str], Mapping[str, str]], +] +HttpxRequestFiles = Union[Mapping[str, HttpxFileTypes], Sequence[Tuple[str, HttpxFileTypes]]] + +# Workaround to support (cast_to: Type[ResponseT]) -> ResponseT +# where ResponseT includes `None`. In order to support directly +# passing `None`, overloads would have to be defined for every +# method that uses `ResponseT` which would lead to an unacceptable +# amount of code duplication and make it unreadable. See _base_client.py +# for example usage. +# +# This unfortunately means that you will either have +# to import this type and pass it explicitly: +# +# from writer_ai import NoneType +# client.get('/foo', cast_to=NoneType) +# +# or build it yourself: +# +# client.get('/foo', cast_to=type(None)) +if TYPE_CHECKING: + NoneType: Type[None] +else: + NoneType = type(None) + + +class RequestOptions(TypedDict, total=False): + headers: Headers + max_retries: int + timeout: float | Timeout | None + params: Query + extra_json: AnyMapping + idempotency_key: str + + +# Sentinel class used until PEP 0661 is accepted +class NotGiven: + """ + A sentinel singleton class used to distinguish omitted keyword arguments + from those passed in with the value None (which may have different behavior). + + For example: + + ```py + def get(timeout: Union[int, NotGiven, None] = NotGiven()) -> Response: + ... + + + get(timeout=1) # 1s timeout + get(timeout=None) # No timeout + get() # Default timeout behavior, which may not be statically known at the method definition. + ``` + """ + + def __bool__(self) -> Literal[False]: + return False + + @override + def __repr__(self) -> str: + return "NOT_GIVEN" + + +NotGivenOr = Union[_T, NotGiven] +NOT_GIVEN = NotGiven() + + +class Omit: + """In certain situations you need to be able to represent a case where a default value has + to be explicitly removed and `None` is not an appropriate substitute, for example: + + ```py + # as the default `Content-Type` header is `application/json` that will be sent + client.post("/upload/files", files={"file": b"my raw file content"}) + + # you can't explicitly override the header as it has to be dynamically generated + # to look something like: 'multipart/form-data; boundary=0d8382fcf5f8c3be01ca2e11002d2983' + client.post(..., headers={"Content-Type": "multipart/form-data"}) + + # instead you can remove the default `application/json` header by passing Omit + client.post(..., headers={"Content-Type": Omit()}) + ``` + """ + + def __bool__(self) -> Literal[False]: + return False + + +@runtime_checkable +class ModelBuilderProtocol(Protocol): + @classmethod + def build( + cls: type[_T], + *, + response: Response, + data: object, + ) -> _T: + ... + + +Headers = Mapping[str, Union[str, Omit]] + + +class HeadersLikeProtocol(Protocol): + def get(self, __key: str) -> str | None: + ... + + +HeadersLike = Union[Headers, HeadersLikeProtocol] + +ResponseT = TypeVar( + "ResponseT", + bound=Union[ + object, + str, + None, + "BaseModel", + List[Any], + Dict[str, Any], + Response, + ModelBuilderProtocol, + "APIResponse[Any]", + "AsyncAPIResponse[Any]", + ], +) + +StrBytesIntFloat = Union[str, bytes, int, float] + +# Note: copied from Pydantic +# https://github.com/pydantic/pydantic/blob/32ea570bf96e84234d2992e1ddf40ab8a565925a/pydantic/main.py#L49 +IncEx: TypeAlias = "set[int] | set[str] | dict[int, Any] | dict[str, Any] | None" + +PostParser = Callable[[Any], Any] + + +@runtime_checkable +class InheritsGeneric(Protocol): + """Represents a type that has inherited from `Generic` + + The `__orig_bases__` property can be used to determine the resolved + type variable for a given base class. + """ + + __orig_bases__: tuple[_GenericAlias] + + +class _GenericAlias(Protocol): + __origin__: type[object] + + +class HttpxSendArgs(TypedDict, total=False): + auth: httpx.Auth diff --git a/src/writer_ai/_utils/__init__.py b/src/writer_ai/_utils/__init__.py new file mode 100644 index 00000000..31b5b227 --- /dev/null +++ b/src/writer_ai/_utils/__init__.py @@ -0,0 +1,51 @@ +from ._sync import asyncify as asyncify +from ._proxy import LazyProxy as LazyProxy +from ._utils import ( + flatten as flatten, + is_dict as is_dict, + is_list as is_list, + is_given as is_given, + is_tuple as is_tuple, + lru_cache as lru_cache, + is_mapping as is_mapping, + is_tuple_t as is_tuple_t, + parse_date as parse_date, + is_iterable as is_iterable, + is_sequence as is_sequence, + coerce_float as coerce_float, + is_mapping_t as is_mapping_t, + removeprefix as removeprefix, + removesuffix as removesuffix, + extract_files as extract_files, + is_sequence_t as is_sequence_t, + required_args as required_args, + coerce_boolean as coerce_boolean, + coerce_integer as coerce_integer, + file_from_path as file_from_path, + parse_datetime as parse_datetime, + strip_not_given as strip_not_given, + deepcopy_minimal as deepcopy_minimal, + get_async_library as get_async_library, + maybe_coerce_float as maybe_coerce_float, + get_required_header as get_required_header, + maybe_coerce_boolean as maybe_coerce_boolean, + maybe_coerce_integer as maybe_coerce_integer, +) +from ._typing import ( + is_list_type as is_list_type, + is_union_type as is_union_type, + extract_type_arg as extract_type_arg, + is_iterable_type as is_iterable_type, + is_required_type as is_required_type, + is_annotated_type as is_annotated_type, + strip_annotated_type as strip_annotated_type, + extract_type_var_from_base as extract_type_var_from_base, +) +from ._streams import consume_sync_iterator as consume_sync_iterator, consume_async_iterator as consume_async_iterator +from ._transform import ( + PropertyInfo as PropertyInfo, + transform as transform, + async_transform as async_transform, + maybe_transform as maybe_transform, + async_maybe_transform as async_maybe_transform, +) diff --git a/src/writer_ai/_utils/_logs.py b/src/writer_ai/_utils/_logs.py new file mode 100644 index 00000000..694fbaa4 --- /dev/null +++ b/src/writer_ai/_utils/_logs.py @@ -0,0 +1,25 @@ +import os +import logging + +logger: logging.Logger = logging.getLogger("writer_ai") +httpx_logger: logging.Logger = logging.getLogger("httpx") + + +def _basic_config() -> None: + # e.g. [2023-10-05 14:12:26 - writer_ai._base_client:818 - DEBUG] HTTP Request: POST http://127.0.0.1:4010/foo/bar "200 OK" + logging.basicConfig( + format="[%(asctime)s - %(name)s:%(lineno)d - %(levelname)s] %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + + +def setup_logging() -> None: + env = os.environ.get("WRITER_AI_LOG") + if env == "debug": + _basic_config() + logger.setLevel(logging.DEBUG) + httpx_logger.setLevel(logging.DEBUG) + elif env == "info": + _basic_config() + logger.setLevel(logging.INFO) + httpx_logger.setLevel(logging.INFO) diff --git a/src/writer_ai/_utils/_proxy.py b/src/writer_ai/_utils/_proxy.py new file mode 100644 index 00000000..c46a62a6 --- /dev/null +++ b/src/writer_ai/_utils/_proxy.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Generic, TypeVar, Iterable, cast +from typing_extensions import override + +T = TypeVar("T") + + +class LazyProxy(Generic[T], ABC): + """Implements data methods to pretend that an instance is another instance. + + This includes forwarding attribute access and other methods. + """ + + # Note: we have to special case proxies that themselves return proxies + # to support using a proxy as a catch-all for any random access, e.g. `proxy.foo.bar.baz` + + def __getattr__(self, attr: str) -> object: + proxied = self.__get_proxied__() + if isinstance(proxied, LazyProxy): + return proxied # pyright: ignore + return getattr(proxied, attr) + + @override + def __repr__(self) -> str: + proxied = self.__get_proxied__() + if isinstance(proxied, LazyProxy): + return proxied.__class__.__name__ + return repr(self.__get_proxied__()) + + @override + def __str__(self) -> str: + proxied = self.__get_proxied__() + if isinstance(proxied, LazyProxy): + return proxied.__class__.__name__ + return str(proxied) + + @override + def __dir__(self) -> Iterable[str]: + proxied = self.__get_proxied__() + if isinstance(proxied, LazyProxy): + return [] + return proxied.__dir__() + + @property # type: ignore + @override + def __class__(self) -> type: # pyright: ignore + proxied = self.__get_proxied__() + if issubclass(type(proxied), LazyProxy): + return type(proxied) + return proxied.__class__ + + def __get_proxied__(self) -> T: + return self.__load__() + + def __as_proxied__(self) -> T: + """Helper method that returns the current proxy, typed as the loaded object""" + return cast(T, self) + + @abstractmethod + def __load__(self) -> T: + ... diff --git a/src/writer_ai/_utils/_streams.py b/src/writer_ai/_utils/_streams.py new file mode 100644 index 00000000..f4a0208f --- /dev/null +++ b/src/writer_ai/_utils/_streams.py @@ -0,0 +1,12 @@ +from typing import Any +from typing_extensions import Iterator, AsyncIterator + + +def consume_sync_iterator(iterator: Iterator[Any]) -> None: + for _ in iterator: + ... + + +async def consume_async_iterator(iterator: AsyncIterator[Any]) -> None: + async for _ in iterator: + ... diff --git a/src/writer_ai/_utils/_sync.py b/src/writer_ai/_utils/_sync.py new file mode 100644 index 00000000..595924e5 --- /dev/null +++ b/src/writer_ai/_utils/_sync.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +import functools +from typing import TypeVar, Callable, Awaitable +from typing_extensions import ParamSpec + +import anyio +import anyio.to_thread + +T_Retval = TypeVar("T_Retval") +T_ParamSpec = ParamSpec("T_ParamSpec") + + +# copied from `asyncer`, https://github.com/tiangolo/asyncer +def asyncify( + function: Callable[T_ParamSpec, T_Retval], + *, + cancellable: bool = False, + limiter: anyio.CapacityLimiter | None = None, +) -> Callable[T_ParamSpec, Awaitable[T_Retval]]: + """ + Take a blocking function and create an async one that receives the same + positional and keyword arguments, and that when called, calls the original function + in a worker thread using `anyio.to_thread.run_sync()`. Internally, + `asyncer.asyncify()` uses the same `anyio.to_thread.run_sync()`, but it supports + keyword arguments additional to positional arguments and it adds better support for + autocompletion and inline errors for the arguments of the function called and the + return value. + + If the `cancellable` option is enabled and the task waiting for its completion is + cancelled, the thread will still run its course but its return value (or any raised + exception) will be ignored. + + Use it like this: + + ```Python + def do_work(arg1, arg2, kwarg1="", kwarg2="") -> str: + # Do work + return "Some result" + + + result = await to_thread.asyncify(do_work)("spam", "ham", kwarg1="a", kwarg2="b") + print(result) + ``` + + ## Arguments + + `function`: a blocking regular callable (e.g. a function) + `cancellable`: `True` to allow cancellation of the operation + `limiter`: capacity limiter to use to limit the total amount of threads running + (if omitted, the default limiter is used) + + ## Return + + An async function that takes the same positional and keyword arguments as the + original one, that when called runs the same original function in a thread worker + and returns the result. + """ + + async def wrapper(*args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs) -> T_Retval: + partial_f = functools.partial(function, *args, **kwargs) + return await anyio.to_thread.run_sync(partial_f, cancellable=cancellable, limiter=limiter) + + return wrapper diff --git a/src/writer_ai/_utils/_transform.py b/src/writer_ai/_utils/_transform.py new file mode 100644 index 00000000..47e262a5 --- /dev/null +++ b/src/writer_ai/_utils/_transform.py @@ -0,0 +1,382 @@ +from __future__ import annotations + +import io +import base64 +import pathlib +from typing import Any, Mapping, TypeVar, cast +from datetime import date, datetime +from typing_extensions import Literal, get_args, override, get_type_hints + +import anyio +import pydantic + +from ._utils import ( + is_list, + is_mapping, + is_iterable, +) +from .._files import is_base64_file_input +from ._typing import ( + is_list_type, + is_union_type, + extract_type_arg, + is_iterable_type, + is_required_type, + is_annotated_type, + strip_annotated_type, +) +from .._compat import model_dump, is_typeddict + +_T = TypeVar("_T") + + +# TODO: support for drilling globals() and locals() +# TODO: ensure works correctly with forward references in all cases + + +PropertyFormat = Literal["iso8601", "base64", "custom"] + + +class PropertyInfo: + """Metadata class to be used in Annotated types to provide information about a given type. + + For example: + + class MyParams(TypedDict): + account_holder_name: Annotated[str, PropertyInfo(alias='accountHolderName')] + + This means that {'account_holder_name': 'Robert'} will be transformed to {'accountHolderName': 'Robert'} before being sent to the API. + """ + + alias: str | None + format: PropertyFormat | None + format_template: str | None + discriminator: str | None + + def __init__( + self, + *, + alias: str | None = None, + format: PropertyFormat | None = None, + format_template: str | None = None, + discriminator: str | None = None, + ) -> None: + self.alias = alias + self.format = format + self.format_template = format_template + self.discriminator = discriminator + + @override + def __repr__(self) -> str: + return f"{self.__class__.__name__}(alias='{self.alias}', format={self.format}, format_template='{self.format_template}', discriminator='{self.discriminator}')" + + +def maybe_transform( + data: object, + expected_type: object, +) -> Any | None: + """Wrapper over `transform()` that allows `None` to be passed. + + See `transform()` for more details. + """ + if data is None: + return None + return transform(data, expected_type) + + +# Wrapper over _transform_recursive providing fake types +def transform( + data: _T, + expected_type: object, +) -> _T: + """Transform dictionaries based off of type information from the given type, for example: + + ```py + class Params(TypedDict, total=False): + card_id: Required[Annotated[str, PropertyInfo(alias="cardID")]] + + + transformed = transform({"card_id": ""}, Params) + # {'cardID': ''} + ``` + + Any keys / data that does not have type information given will be included as is. + + It should be noted that the transformations that this function does are not represented in the type system. + """ + transformed = _transform_recursive(data, annotation=cast(type, expected_type)) + return cast(_T, transformed) + + +def _get_annotated_type(type_: type) -> type | None: + """If the given type is an `Annotated` type then it is returned, if not `None` is returned. + + This also unwraps the type when applicable, e.g. `Required[Annotated[T, ...]]` + """ + if is_required_type(type_): + # Unwrap `Required[Annotated[T, ...]]` to `Annotated[T, ...]` + type_ = get_args(type_)[0] + + if is_annotated_type(type_): + return type_ + + return None + + +def _maybe_transform_key(key: str, type_: type) -> str: + """Transform the given `data` based on the annotations provided in `type_`. + + Note: this function only looks at `Annotated` types that contain `PropertInfo` metadata. + """ + annotated_type = _get_annotated_type(type_) + if annotated_type is None: + # no `Annotated` definition for this type, no transformation needed + return key + + # ignore the first argument as it is the actual type + annotations = get_args(annotated_type)[1:] + for annotation in annotations: + if isinstance(annotation, PropertyInfo) and annotation.alias is not None: + return annotation.alias + + return key + + +def _transform_recursive( + data: object, + *, + annotation: type, + inner_type: type | None = None, +) -> object: + """Transform the given data against the expected type. + + Args: + annotation: The direct type annotation given to the particular piece of data. + This may or may not be wrapped in metadata types, e.g. `Required[T]`, `Annotated[T, ...]` etc + + inner_type: If applicable, this is the "inside" type. This is useful in certain cases where the outside type + is a container type such as `List[T]`. In that case `inner_type` should be set to `T` so that each entry in + the list can be transformed using the metadata from the container type. + + Defaults to the same value as the `annotation` argument. + """ + if inner_type is None: + inner_type = annotation + + stripped_type = strip_annotated_type(inner_type) + if is_typeddict(stripped_type) and is_mapping(data): + return _transform_typeddict(data, stripped_type) + + if ( + # List[T] + (is_list_type(stripped_type) and is_list(data)) + # Iterable[T] + or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str)) + ): + inner_type = extract_type_arg(stripped_type, 0) + return [_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data] + + if is_union_type(stripped_type): + # For union types we run the transformation against all subtypes to ensure that everything is transformed. + # + # TODO: there may be edge cases where the same normalized field name will transform to two different names + # in different subtypes. + for subtype in get_args(stripped_type): + data = _transform_recursive(data, annotation=annotation, inner_type=subtype) + return data + + if isinstance(data, pydantic.BaseModel): + return model_dump(data, exclude_unset=True) + + annotated_type = _get_annotated_type(annotation) + if annotated_type is None: + return data + + # ignore the first argument as it is the actual type + annotations = get_args(annotated_type)[1:] + for annotation in annotations: + if isinstance(annotation, PropertyInfo) and annotation.format is not None: + return _format_data(data, annotation.format, annotation.format_template) + + return data + + +def _format_data(data: object, format_: PropertyFormat, format_template: str | None) -> object: + if isinstance(data, (date, datetime)): + if format_ == "iso8601": + return data.isoformat() + + if format_ == "custom" and format_template is not None: + return data.strftime(format_template) + + if format_ == "base64" and is_base64_file_input(data): + binary: str | bytes | None = None + + if isinstance(data, pathlib.Path): + binary = data.read_bytes() + elif isinstance(data, io.IOBase): + binary = data.read() + + if isinstance(binary, str): # type: ignore[unreachable] + binary = binary.encode() + + if not isinstance(binary, bytes): + raise RuntimeError(f"Could not read bytes from {data}; Received {type(binary)}") + + return base64.b64encode(binary).decode("ascii") + + return data + + +def _transform_typeddict( + data: Mapping[str, object], + expected_type: type, +) -> Mapping[str, object]: + result: dict[str, object] = {} + annotations = get_type_hints(expected_type, include_extras=True) + for key, value in data.items(): + type_ = annotations.get(key) + if type_ is None: + # we do not have a type annotation for this field, leave it as is + result[key] = value + else: + result[_maybe_transform_key(key, type_)] = _transform_recursive(value, annotation=type_) + return result + + +async def async_maybe_transform( + data: object, + expected_type: object, +) -> Any | None: + """Wrapper over `async_transform()` that allows `None` to be passed. + + See `async_transform()` for more details. + """ + if data is None: + return None + return await async_transform(data, expected_type) + + +async def async_transform( + data: _T, + expected_type: object, +) -> _T: + """Transform dictionaries based off of type information from the given type, for example: + + ```py + class Params(TypedDict, total=False): + card_id: Required[Annotated[str, PropertyInfo(alias="cardID")]] + + + transformed = transform({"card_id": ""}, Params) + # {'cardID': ''} + ``` + + Any keys / data that does not have type information given will be included as is. + + It should be noted that the transformations that this function does are not represented in the type system. + """ + transformed = await _async_transform_recursive(data, annotation=cast(type, expected_type)) + return cast(_T, transformed) + + +async def _async_transform_recursive( + data: object, + *, + annotation: type, + inner_type: type | None = None, +) -> object: + """Transform the given data against the expected type. + + Args: + annotation: The direct type annotation given to the particular piece of data. + This may or may not be wrapped in metadata types, e.g. `Required[T]`, `Annotated[T, ...]` etc + + inner_type: If applicable, this is the "inside" type. This is useful in certain cases where the outside type + is a container type such as `List[T]`. In that case `inner_type` should be set to `T` so that each entry in + the list can be transformed using the metadata from the container type. + + Defaults to the same value as the `annotation` argument. + """ + if inner_type is None: + inner_type = annotation + + stripped_type = strip_annotated_type(inner_type) + if is_typeddict(stripped_type) and is_mapping(data): + return await _async_transform_typeddict(data, stripped_type) + + if ( + # List[T] + (is_list_type(stripped_type) and is_list(data)) + # Iterable[T] + or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str)) + ): + inner_type = extract_type_arg(stripped_type, 0) + return [await _async_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data] + + if is_union_type(stripped_type): + # For union types we run the transformation against all subtypes to ensure that everything is transformed. + # + # TODO: there may be edge cases where the same normalized field name will transform to two different names + # in different subtypes. + for subtype in get_args(stripped_type): + data = await _async_transform_recursive(data, annotation=annotation, inner_type=subtype) + return data + + if isinstance(data, pydantic.BaseModel): + return model_dump(data, exclude_unset=True) + + annotated_type = _get_annotated_type(annotation) + if annotated_type is None: + return data + + # ignore the first argument as it is the actual type + annotations = get_args(annotated_type)[1:] + for annotation in annotations: + if isinstance(annotation, PropertyInfo) and annotation.format is not None: + return await _async_format_data(data, annotation.format, annotation.format_template) + + return data + + +async def _async_format_data(data: object, format_: PropertyFormat, format_template: str | None) -> object: + if isinstance(data, (date, datetime)): + if format_ == "iso8601": + return data.isoformat() + + if format_ == "custom" and format_template is not None: + return data.strftime(format_template) + + if format_ == "base64" and is_base64_file_input(data): + binary: str | bytes | None = None + + if isinstance(data, pathlib.Path): + binary = await anyio.Path(data).read_bytes() + elif isinstance(data, io.IOBase): + binary = data.read() + + if isinstance(binary, str): # type: ignore[unreachable] + binary = binary.encode() + + if not isinstance(binary, bytes): + raise RuntimeError(f"Could not read bytes from {data}; Received {type(binary)}") + + return base64.b64encode(binary).decode("ascii") + + return data + + +async def _async_transform_typeddict( + data: Mapping[str, object], + expected_type: type, +) -> Mapping[str, object]: + result: dict[str, object] = {} + annotations = get_type_hints(expected_type, include_extras=True) + for key, value in data.items(): + type_ = annotations.get(key) + if type_ is None: + # we do not have a type annotation for this field, leave it as is + result[key] = value + else: + result[_maybe_transform_key(key, type_)] = await _async_transform_recursive(value, annotation=type_) + return result diff --git a/src/writer_ai/_utils/_typing.py b/src/writer_ai/_utils/_typing.py new file mode 100644 index 00000000..c036991f --- /dev/null +++ b/src/writer_ai/_utils/_typing.py @@ -0,0 +1,120 @@ +from __future__ import annotations + +from typing import Any, TypeVar, Iterable, cast +from collections import abc as _c_abc +from typing_extensions import Required, Annotated, get_args, get_origin + +from .._types import InheritsGeneric +from .._compat import is_union as _is_union + + +def is_annotated_type(typ: type) -> bool: + return get_origin(typ) == Annotated + + +def is_list_type(typ: type) -> bool: + return (get_origin(typ) or typ) == list + + +def is_iterable_type(typ: type) -> bool: + """If the given type is `typing.Iterable[T]`""" + origin = get_origin(typ) or typ + return origin == Iterable or origin == _c_abc.Iterable + + +def is_union_type(typ: type) -> bool: + return _is_union(get_origin(typ)) + + +def is_required_type(typ: type) -> bool: + return get_origin(typ) == Required + + +def is_typevar(typ: type) -> bool: + # type ignore is required because type checkers + # think this expression will always return False + return type(typ) == TypeVar # type: ignore + + +# Extracts T from Annotated[T, ...] or from Required[Annotated[T, ...]] +def strip_annotated_type(typ: type) -> type: + if is_required_type(typ) or is_annotated_type(typ): + return strip_annotated_type(cast(type, get_args(typ)[0])) + + return typ + + +def extract_type_arg(typ: type, index: int) -> type: + args = get_args(typ) + try: + return cast(type, args[index]) + except IndexError as err: + raise RuntimeError(f"Expected type {typ} to have a type argument at index {index} but it did not") from err + + +def extract_type_var_from_base( + typ: type, + *, + generic_bases: tuple[type, ...], + index: int, + failure_message: str | None = None, +) -> type: + """Given a type like `Foo[T]`, returns the generic type variable `T`. + + This also handles the case where a concrete subclass is given, e.g. + ```py + class MyResponse(Foo[bytes]): + ... + + extract_type_var(MyResponse, bases=(Foo,), index=0) -> bytes + ``` + + And where a generic subclass is given: + ```py + _T = TypeVar('_T') + class MyResponse(Foo[_T]): + ... + + extract_type_var(MyResponse[bytes], bases=(Foo,), index=0) -> bytes + ``` + """ + cls = cast(object, get_origin(typ) or typ) + if cls in generic_bases: + # we're given the class directly + return extract_type_arg(typ, index) + + # if a subclass is given + # --- + # this is needed as __orig_bases__ is not present in the typeshed stubs + # because it is intended to be for internal use only, however there does + # not seem to be a way to resolve generic TypeVars for inherited subclasses + # without using it. + if isinstance(cls, InheritsGeneric): + target_base_class: Any | None = None + for base in cls.__orig_bases__: + if base.__origin__ in generic_bases: + target_base_class = base + break + + if target_base_class is None: + raise RuntimeError( + "Could not find the generic base class;\n" + "This should never happen;\n" + f"Does {cls} inherit from one of {generic_bases} ?" + ) + + extracted = extract_type_arg(target_base_class, index) + if is_typevar(extracted): + # If the extracted type argument is itself a type variable + # then that means the subclass itself is generic, so we have + # to resolve the type argument from the class itself, not + # the base class. + # + # Note: if there is more than 1 type argument, the subclass could + # change the ordering of the type arguments, this is not currently + # supported. + return extract_type_arg(typ, index) + + return extracted + + raise RuntimeError(failure_message or f"Could not resolve inner type variable at index {index} for {typ}") diff --git a/src/writer_ai/_utils/_utils.py b/src/writer_ai/_utils/_utils.py new file mode 100644 index 00000000..17904ce6 --- /dev/null +++ b/src/writer_ai/_utils/_utils.py @@ -0,0 +1,403 @@ +from __future__ import annotations + +import os +import re +import inspect +import functools +from typing import ( + Any, + Tuple, + Mapping, + TypeVar, + Callable, + Iterable, + Sequence, + cast, + overload, +) +from pathlib import Path +from typing_extensions import TypeGuard + +import sniffio + +from .._types import Headers, NotGiven, FileTypes, NotGivenOr, HeadersLike +from .._compat import parse_date as parse_date, parse_datetime as parse_datetime + +_T = TypeVar("_T") +_TupleT = TypeVar("_TupleT", bound=Tuple[object, ...]) +_MappingT = TypeVar("_MappingT", bound=Mapping[str, object]) +_SequenceT = TypeVar("_SequenceT", bound=Sequence[object]) +CallableT = TypeVar("CallableT", bound=Callable[..., Any]) + + +def flatten(t: Iterable[Iterable[_T]]) -> list[_T]: + return [item for sublist in t for item in sublist] + + +def extract_files( + # TODO: this needs to take Dict but variance issues..... + # create protocol type ? + query: Mapping[str, object], + *, + paths: Sequence[Sequence[str]], +) -> list[tuple[str, FileTypes]]: + """Recursively extract files from the given dictionary based on specified paths. + + A path may look like this ['foo', 'files', '', 'data']. + + Note: this mutates the given dictionary. + """ + files: list[tuple[str, FileTypes]] = [] + for path in paths: + files.extend(_extract_items(query, path, index=0, flattened_key=None)) + return files + + +def _extract_items( + obj: object, + path: Sequence[str], + *, + index: int, + flattened_key: str | None, +) -> list[tuple[str, FileTypes]]: + try: + key = path[index] + except IndexError: + if isinstance(obj, NotGiven): + # no value was provided - we can safely ignore + return [] + + # cyclical import + from .._files import assert_is_file_content + + # We have exhausted the path, return the entry we found. + assert_is_file_content(obj, key=flattened_key) + assert flattened_key is not None + return [(flattened_key, cast(FileTypes, obj))] + + index += 1 + if is_dict(obj): + try: + # We are at the last entry in the path so we must remove the field + if (len(path)) == index: + item = obj.pop(key) + else: + item = obj[key] + except KeyError: + # Key was not present in the dictionary, this is not indicative of an error + # as the given path may not point to a required field. We also do not want + # to enforce required fields as the API may differ from the spec in some cases. + return [] + if flattened_key is None: + flattened_key = key + else: + flattened_key += f"[{key}]" + return _extract_items( + item, + path, + index=index, + flattened_key=flattened_key, + ) + elif is_list(obj): + if key != "": + return [] + + return flatten( + [ + _extract_items( + item, + path, + index=index, + flattened_key=flattened_key + "[]" if flattened_key is not None else "[]", + ) + for item in obj + ] + ) + + # Something unexpected was passed, just ignore it. + return [] + + +def is_given(obj: NotGivenOr[_T]) -> TypeGuard[_T]: + return not isinstance(obj, NotGiven) + + +# Type safe methods for narrowing types with TypeVars. +# The default narrowing for isinstance(obj, dict) is dict[unknown, unknown], +# however this cause Pyright to rightfully report errors. As we know we don't +# care about the contained types we can safely use `object` in it's place. +# +# There are two separate functions defined, `is_*` and `is_*_t` for different use cases. +# `is_*` is for when you're dealing with an unknown input +# `is_*_t` is for when you're narrowing a known union type to a specific subset + + +def is_tuple(obj: object) -> TypeGuard[tuple[object, ...]]: + return isinstance(obj, tuple) + + +def is_tuple_t(obj: _TupleT | object) -> TypeGuard[_TupleT]: + return isinstance(obj, tuple) + + +def is_sequence(obj: object) -> TypeGuard[Sequence[object]]: + return isinstance(obj, Sequence) + + +def is_sequence_t(obj: _SequenceT | object) -> TypeGuard[_SequenceT]: + return isinstance(obj, Sequence) + + +def is_mapping(obj: object) -> TypeGuard[Mapping[str, object]]: + return isinstance(obj, Mapping) + + +def is_mapping_t(obj: _MappingT | object) -> TypeGuard[_MappingT]: + return isinstance(obj, Mapping) + + +def is_dict(obj: object) -> TypeGuard[dict[object, object]]: + return isinstance(obj, dict) + + +def is_list(obj: object) -> TypeGuard[list[object]]: + return isinstance(obj, list) + + +def is_iterable(obj: object) -> TypeGuard[Iterable[object]]: + return isinstance(obj, Iterable) + + +def deepcopy_minimal(item: _T) -> _T: + """Minimal reimplementation of copy.deepcopy() that will only copy certain object types: + + - mappings, e.g. `dict` + - list + + This is done for performance reasons. + """ + if is_mapping(item): + return cast(_T, {k: deepcopy_minimal(v) for k, v in item.items()}) + if is_list(item): + return cast(_T, [deepcopy_minimal(entry) for entry in item]) + return item + + +# copied from https://github.com/Rapptz/RoboDanny +def human_join(seq: Sequence[str], *, delim: str = ", ", final: str = "or") -> str: + size = len(seq) + if size == 0: + return "" + + if size == 1: + return seq[0] + + if size == 2: + return f"{seq[0]} {final} {seq[1]}" + + return delim.join(seq[:-1]) + f" {final} {seq[-1]}" + + +def quote(string: str) -> str: + """Add single quotation marks around the given string. Does *not* do any escaping.""" + return f"'{string}'" + + +def required_args(*variants: Sequence[str]) -> Callable[[CallableT], CallableT]: + """Decorator to enforce a given set of arguments or variants of arguments are passed to the decorated function. + + Useful for enforcing runtime validation of overloaded functions. + + Example usage: + ```py + @overload + def foo(*, a: str) -> str: + ... + + + @overload + def foo(*, b: bool) -> str: + ... + + + # This enforces the same constraints that a static type checker would + # i.e. that either a or b must be passed to the function + @required_args(["a"], ["b"]) + def foo(*, a: str | None = None, b: bool | None = None) -> str: + ... + ``` + """ + + def inner(func: CallableT) -> CallableT: + params = inspect.signature(func).parameters + positional = [ + name + for name, param in params.items() + if param.kind + in { + param.POSITIONAL_ONLY, + param.POSITIONAL_OR_KEYWORD, + } + ] + + @functools.wraps(func) + def wrapper(*args: object, **kwargs: object) -> object: + given_params: set[str] = set() + for i, _ in enumerate(args): + try: + given_params.add(positional[i]) + except IndexError: + raise TypeError( + f"{func.__name__}() takes {len(positional)} argument(s) but {len(args)} were given" + ) from None + + for key in kwargs.keys(): + given_params.add(key) + + for variant in variants: + matches = all((param in given_params for param in variant)) + if matches: + break + else: # no break + if len(variants) > 1: + variations = human_join( + ["(" + human_join([quote(arg) for arg in variant], final="and") + ")" for variant in variants] + ) + msg = f"Missing required arguments; Expected either {variations} arguments to be given" + else: + assert len(variants) > 0 + + # TODO: this error message is not deterministic + missing = list(set(variants[0]) - given_params) + if len(missing) > 1: + msg = f"Missing required arguments: {human_join([quote(arg) for arg in missing])}" + else: + msg = f"Missing required argument: {quote(missing[0])}" + raise TypeError(msg) + return func(*args, **kwargs) + + return wrapper # type: ignore + + return inner + + +_K = TypeVar("_K") +_V = TypeVar("_V") + + +@overload +def strip_not_given(obj: None) -> None: + ... + + +@overload +def strip_not_given(obj: Mapping[_K, _V | NotGiven]) -> dict[_K, _V]: + ... + + +@overload +def strip_not_given(obj: object) -> object: + ... + + +def strip_not_given(obj: object | None) -> object: + """Remove all top-level keys where their values are instances of `NotGiven`""" + if obj is None: + return None + + if not is_mapping(obj): + return obj + + return {key: value for key, value in obj.items() if not isinstance(value, NotGiven)} + + +def coerce_integer(val: str) -> int: + return int(val, base=10) + + +def coerce_float(val: str) -> float: + return float(val) + + +def coerce_boolean(val: str) -> bool: + return val == "true" or val == "1" or val == "on" + + +def maybe_coerce_integer(val: str | None) -> int | None: + if val is None: + return None + return coerce_integer(val) + + +def maybe_coerce_float(val: str | None) -> float | None: + if val is None: + return None + return coerce_float(val) + + +def maybe_coerce_boolean(val: str | None) -> bool | None: + if val is None: + return None + return coerce_boolean(val) + + +def removeprefix(string: str, prefix: str) -> str: + """Remove a prefix from a string. + + Backport of `str.removeprefix` for Python < 3.9 + """ + if string.startswith(prefix): + return string[len(prefix) :] + return string + + +def removesuffix(string: str, suffix: str) -> str: + """Remove a suffix from a string. + + Backport of `str.removesuffix` for Python < 3.9 + """ + if string.endswith(suffix): + return string[: -len(suffix)] + return string + + +def file_from_path(path: str) -> FileTypes: + contents = Path(path).read_bytes() + file_name = os.path.basename(path) + return (file_name, contents) + + +def get_required_header(headers: HeadersLike, header: str) -> str: + lower_header = header.lower() + if isinstance(headers, Mapping): + headers = cast(Headers, headers) + for k, v in headers.items(): + if k.lower() == lower_header and isinstance(v, str): + return v + + """ to deal with the case where the header looks like Stainless-Event-Id """ + intercaps_header = re.sub(r"([^\w])(\w)", lambda pat: pat.group(1) + pat.group(2).upper(), header.capitalize()) + + for normalized_header in [header, lower_header, header.upper(), intercaps_header]: + value = headers.get(normalized_header) + if value: + return value + + raise ValueError(f"Could not find {header} header") + + +def get_async_library() -> str: + try: + return sniffio.current_async_library() + except Exception: + return "false" + + +def lru_cache(*, maxsize: int | None = 128) -> Callable[[CallableT], CallableT]: + """A version of functools.lru_cache that retains the type signature + for the wrapped function arguments. + """ + wrapper = functools.lru_cache( # noqa: TID251 + maxsize=maxsize, + ) + return cast(Any, wrapper) # type: ignore[no-any-return] diff --git a/src/writer_ai/_version.py b/src/writer_ai/_version.py new file mode 100644 index 00000000..47b03f45 --- /dev/null +++ b/src/writer_ai/_version.py @@ -0,0 +1,4 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +__title__ = "writer_ai" +__version__ = "0.0.1-alpha.0" diff --git a/src/writer_ai/lib/.keep b/src/writer_ai/lib/.keep new file mode 100644 index 00000000..5e2c99fd --- /dev/null +++ b/src/writer_ai/lib/.keep @@ -0,0 +1,4 @@ +File generated from our OpenAPI spec by Stainless. + +This directory can be used to store custom files to expand the SDK. +It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. \ No newline at end of file diff --git a/src/writer_ai/py.typed b/src/writer_ai/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/src/writer_ai/resources/__init__.py b/src/writer_ai/resources/__init__.py new file mode 100644 index 00000000..d4fe7058 --- /dev/null +++ b/src/writer_ai/resources/__init__.py @@ -0,0 +1,47 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .chat import ( + ChatResource, + AsyncChatResource, + ChatResourceWithRawResponse, + AsyncChatResourceWithRawResponse, + ChatResourceWithStreamingResponse, + AsyncChatResourceWithStreamingResponse, +) +from .models import ( + ModelsResource, + AsyncModelsResource, + ModelsResourceWithRawResponse, + AsyncModelsResourceWithRawResponse, + ModelsResourceWithStreamingResponse, + AsyncModelsResourceWithStreamingResponse, +) +from .completions import ( + CompletionsResource, + AsyncCompletionsResource, + CompletionsResourceWithRawResponse, + AsyncCompletionsResourceWithRawResponse, + CompletionsResourceWithStreamingResponse, + AsyncCompletionsResourceWithStreamingResponse, +) + +__all__ = [ + "ChatResource", + "AsyncChatResource", + "ChatResourceWithRawResponse", + "AsyncChatResourceWithRawResponse", + "ChatResourceWithStreamingResponse", + "AsyncChatResourceWithStreamingResponse", + "CompletionsResource", + "AsyncCompletionsResource", + "CompletionsResourceWithRawResponse", + "AsyncCompletionsResourceWithRawResponse", + "CompletionsResourceWithStreamingResponse", + "AsyncCompletionsResourceWithStreamingResponse", + "ModelsResource", + "AsyncModelsResource", + "ModelsResourceWithRawResponse", + "AsyncModelsResourceWithRawResponse", + "ModelsResourceWithStreamingResponse", + "AsyncModelsResourceWithStreamingResponse", +] diff --git a/src/writer_ai/resources/chat.py b/src/writer_ai/resources/chat.py new file mode 100644 index 00000000..2bd2fa14 --- /dev/null +++ b/src/writer_ai/resources/chat.py @@ -0,0 +1,182 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Union, Iterable + +import httpx + +from ..types import chat_chat_params +from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from .._utils import ( + maybe_transform, + async_maybe_transform, +) +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import ( + make_request_options, +) +from ..types.chat_chat_response import ChatChatResponse + +__all__ = ["ChatResource", "AsyncChatResource"] + + +class ChatResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> ChatResourceWithRawResponse: + return ChatResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ChatResourceWithStreamingResponse: + return ChatResourceWithStreamingResponse(self) + + def chat( + self, + *, + messages: Iterable[chat_chat_params.Message], + model: str, + max_tokens: int | NotGiven = NOT_GIVEN, + n: int | NotGiven = NOT_GIVEN, + stop: Union[List[str], str] | NotGiven = NOT_GIVEN, + temperature: float | NotGiven = NOT_GIVEN, + top_p: float | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ChatChatResponse: + """ + Create chat completion + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/v1/chat", + body=maybe_transform( + { + "messages": messages, + "model": model, + "max_tokens": max_tokens, + "n": n, + "stop": stop, + "temperature": temperature, + "top_p": top_p, + }, + chat_chat_params.ChatChatParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ChatChatResponse, + ) + + +class AsyncChatResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncChatResourceWithRawResponse: + return AsyncChatResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncChatResourceWithStreamingResponse: + return AsyncChatResourceWithStreamingResponse(self) + + async def chat( + self, + *, + messages: Iterable[chat_chat_params.Message], + model: str, + max_tokens: int | NotGiven = NOT_GIVEN, + n: int | NotGiven = NOT_GIVEN, + stop: Union[List[str], str] | NotGiven = NOT_GIVEN, + temperature: float | NotGiven = NOT_GIVEN, + top_p: float | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ChatChatResponse: + """ + Create chat completion + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/v1/chat", + body=await async_maybe_transform( + { + "messages": messages, + "model": model, + "max_tokens": max_tokens, + "n": n, + "stop": stop, + "temperature": temperature, + "top_p": top_p, + }, + chat_chat_params.ChatChatParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ChatChatResponse, + ) + + +class ChatResourceWithRawResponse: + def __init__(self, chat: ChatResource) -> None: + self._chat = chat + + self.chat = to_raw_response_wrapper( + chat.chat, + ) + + +class AsyncChatResourceWithRawResponse: + def __init__(self, chat: AsyncChatResource) -> None: + self._chat = chat + + self.chat = async_to_raw_response_wrapper( + chat.chat, + ) + + +class ChatResourceWithStreamingResponse: + def __init__(self, chat: ChatResource) -> None: + self._chat = chat + + self.chat = to_streamed_response_wrapper( + chat.chat, + ) + + +class AsyncChatResourceWithStreamingResponse: + def __init__(self, chat: AsyncChatResource) -> None: + self._chat = chat + + self.chat = async_to_streamed_response_wrapper( + chat.chat, + ) diff --git a/src/writer_ai/resources/completions.py b/src/writer_ai/resources/completions.py new file mode 100644 index 00000000..ce9ebead --- /dev/null +++ b/src/writer_ai/resources/completions.py @@ -0,0 +1,380 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, overload +from typing_extensions import Literal + +import httpx + +from ..types import completion_create_params +from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from .._utils import ( + required_args, + maybe_transform, + async_maybe_transform, +) +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._streaming import Stream, AsyncStream +from .._base_client import ( + make_request_options, +) +from ..types.completion import Completion +from ..types.streaming_data import StreamingData + +__all__ = ["CompletionsResource", "AsyncCompletionsResource"] + + +class CompletionsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> CompletionsResourceWithRawResponse: + return CompletionsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> CompletionsResourceWithStreamingResponse: + return CompletionsResourceWithStreamingResponse(self) + + @overload + def create( + self, + *, + model: str, + prompt: str, + best_of: int | NotGiven = NOT_GIVEN, + max_tokens: int | NotGiven = NOT_GIVEN, + random_seed: int | NotGiven = NOT_GIVEN, + stop: List[str] | NotGiven = NOT_GIVEN, + stream: Literal[False] | NotGiven = NOT_GIVEN, + temperature: float | NotGiven = NOT_GIVEN, + top_p: float | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Completion: + """ + Create completion using LLM model + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + def create( + self, + *, + model: str, + prompt: str, + stream: Literal[True], + best_of: int | NotGiven = NOT_GIVEN, + max_tokens: int | NotGiven = NOT_GIVEN, + random_seed: int | NotGiven = NOT_GIVEN, + stop: List[str] | NotGiven = NOT_GIVEN, + temperature: float | NotGiven = NOT_GIVEN, + top_p: float | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Stream[StreamingData]: + """ + Create completion using LLM model + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + def create( + self, + *, + model: str, + prompt: str, + stream: bool, + best_of: int | NotGiven = NOT_GIVEN, + max_tokens: int | NotGiven = NOT_GIVEN, + random_seed: int | NotGiven = NOT_GIVEN, + stop: List[str] | NotGiven = NOT_GIVEN, + temperature: float | NotGiven = NOT_GIVEN, + top_p: float | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Completion | Stream[StreamingData]: + """ + Create completion using LLM model + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @required_args(["model", "prompt"], ["model", "prompt", "stream"]) + def create( + self, + *, + model: str, + prompt: str, + best_of: int | NotGiven = NOT_GIVEN, + max_tokens: int | NotGiven = NOT_GIVEN, + random_seed: int | NotGiven = NOT_GIVEN, + stop: List[str] | NotGiven = NOT_GIVEN, + stream: Literal[False] | Literal[True] | NotGiven = NOT_GIVEN, + temperature: float | NotGiven = NOT_GIVEN, + top_p: float | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Completion | Stream[StreamingData]: + return self._post( + "/v1/completions", + body=maybe_transform( + { + "model": model, + "prompt": prompt, + "best_of": best_of, + "max_tokens": max_tokens, + "random_seed": random_seed, + "stop": stop, + "stream": stream, + "temperature": temperature, + "top_p": top_p, + }, + completion_create_params.CompletionCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Completion, + stream=stream or False, + stream_cls=Stream[StreamingData], + ) + + +class AsyncCompletionsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncCompletionsResourceWithRawResponse: + return AsyncCompletionsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncCompletionsResourceWithStreamingResponse: + return AsyncCompletionsResourceWithStreamingResponse(self) + + @overload + async def create( + self, + *, + model: str, + prompt: str, + best_of: int | NotGiven = NOT_GIVEN, + max_tokens: int | NotGiven = NOT_GIVEN, + random_seed: int | NotGiven = NOT_GIVEN, + stop: List[str] | NotGiven = NOT_GIVEN, + stream: Literal[False] | NotGiven = NOT_GIVEN, + temperature: float | NotGiven = NOT_GIVEN, + top_p: float | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Completion: + """ + Create completion using LLM model + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + async def create( + self, + *, + model: str, + prompt: str, + stream: Literal[True], + best_of: int | NotGiven = NOT_GIVEN, + max_tokens: int | NotGiven = NOT_GIVEN, + random_seed: int | NotGiven = NOT_GIVEN, + stop: List[str] | NotGiven = NOT_GIVEN, + temperature: float | NotGiven = NOT_GIVEN, + top_p: float | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncStream[StreamingData]: + """ + Create completion using LLM model + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + async def create( + self, + *, + model: str, + prompt: str, + stream: bool, + best_of: int | NotGiven = NOT_GIVEN, + max_tokens: int | NotGiven = NOT_GIVEN, + random_seed: int | NotGiven = NOT_GIVEN, + stop: List[str] | NotGiven = NOT_GIVEN, + temperature: float | NotGiven = NOT_GIVEN, + top_p: float | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Completion | AsyncStream[StreamingData]: + """ + Create completion using LLM model + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @required_args(["model", "prompt"], ["model", "prompt", "stream"]) + async def create( + self, + *, + model: str, + prompt: str, + best_of: int | NotGiven = NOT_GIVEN, + max_tokens: int | NotGiven = NOT_GIVEN, + random_seed: int | NotGiven = NOT_GIVEN, + stop: List[str] | NotGiven = NOT_GIVEN, + stream: Literal[False] | Literal[True] | NotGiven = NOT_GIVEN, + temperature: float | NotGiven = NOT_GIVEN, + top_p: float | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Completion | AsyncStream[StreamingData]: + return await self._post( + "/v1/completions", + body=await async_maybe_transform( + { + "model": model, + "prompt": prompt, + "best_of": best_of, + "max_tokens": max_tokens, + "random_seed": random_seed, + "stop": stop, + "stream": stream, + "temperature": temperature, + "top_p": top_p, + }, + completion_create_params.CompletionCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Completion, + stream=stream or False, + stream_cls=AsyncStream[StreamingData], + ) + + +class CompletionsResourceWithRawResponse: + def __init__(self, completions: CompletionsResource) -> None: + self._completions = completions + + self.create = to_raw_response_wrapper( + completions.create, + ) + + +class AsyncCompletionsResourceWithRawResponse: + def __init__(self, completions: AsyncCompletionsResource) -> None: + self._completions = completions + + self.create = async_to_raw_response_wrapper( + completions.create, + ) + + +class CompletionsResourceWithStreamingResponse: + def __init__(self, completions: CompletionsResource) -> None: + self._completions = completions + + self.create = to_streamed_response_wrapper( + completions.create, + ) + + +class AsyncCompletionsResourceWithStreamingResponse: + def __init__(self, completions: AsyncCompletionsResource) -> None: + self._completions = completions + + self.create = async_to_streamed_response_wrapper( + completions.create, + ) diff --git a/src/writer_ai/resources/models.py b/src/writer_ai/resources/models.py new file mode 100644 index 00000000..906b3f61 --- /dev/null +++ b/src/writer_ai/resources/models.py @@ -0,0 +1,115 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import ( + make_request_options, +) +from ..types.model_list_response import ModelListResponse + +__all__ = ["ModelsResource", "AsyncModelsResource"] + + +class ModelsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> ModelsResourceWithRawResponse: + return ModelsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ModelsResourceWithStreamingResponse: + return ModelsResourceWithStreamingResponse(self) + + def list( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ModelListResponse: + """List the available models""" + return self._get( + "/v1/models", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ModelListResponse, + ) + + +class AsyncModelsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncModelsResourceWithRawResponse: + return AsyncModelsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncModelsResourceWithStreamingResponse: + return AsyncModelsResourceWithStreamingResponse(self) + + async def list( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ModelListResponse: + """List the available models""" + return await self._get( + "/v1/models", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ModelListResponse, + ) + + +class ModelsResourceWithRawResponse: + def __init__(self, models: ModelsResource) -> None: + self._models = models + + self.list = to_raw_response_wrapper( + models.list, + ) + + +class AsyncModelsResourceWithRawResponse: + def __init__(self, models: AsyncModelsResource) -> None: + self._models = models + + self.list = async_to_raw_response_wrapper( + models.list, + ) + + +class ModelsResourceWithStreamingResponse: + def __init__(self, models: ModelsResource) -> None: + self._models = models + + self.list = to_streamed_response_wrapper( + models.list, + ) + + +class AsyncModelsResourceWithStreamingResponse: + def __init__(self, models: AsyncModelsResource) -> None: + self._models = models + + self.list = async_to_streamed_response_wrapper( + models.list, + ) diff --git a/src/writer_ai/types/__init__.py b/src/writer_ai/types/__init__.py new file mode 100644 index 00000000..82199ece --- /dev/null +++ b/src/writer_ai/types/__init__.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .completion import Completion as Completion +from .streaming_data import StreamingData as StreamingData +from .chat_chat_params import ChatChatParams as ChatChatParams +from .chat_chat_response import ChatChatResponse as ChatChatResponse +from .model_list_response import ModelListResponse as ModelListResponse +from .completion_create_params import CompletionCreateParams as CompletionCreateParams diff --git a/src/writer_ai/types/chat_chat_params.py b/src/writer_ai/types/chat_chat_params.py new file mode 100644 index 00000000..f3a9aa9b --- /dev/null +++ b/src/writer_ai/types/chat_chat_params.py @@ -0,0 +1,32 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Union, Iterable +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["ChatChatParams", "Message"] + + +class ChatChatParams(TypedDict, total=False): + messages: Required[Iterable[Message]] + + model: Required[str] + + max_tokens: int + + n: int + + stop: Union[List[str], str] + + temperature: float + + top_p: float + + +class Message(TypedDict, total=False): + content: Required[str] + + role: Required[Literal["user", "assistant", "system"]] + + name: str diff --git a/src/writer_ai/types/chat_chat_response.py b/src/writer_ai/types/chat_chat_response.py new file mode 100644 index 00000000..6f4bb916 --- /dev/null +++ b/src/writer_ai/types/chat_chat_response.py @@ -0,0 +1,30 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["ChatChatResponse", "Choice", "ChoiceMessage"] + + +class ChoiceMessage(BaseModel): + content: str + + role: Literal["user", "assistant", "system"] + + +class Choice(BaseModel): + finish_reason: Literal["stop", "length", "content_filter"] + + message: ChoiceMessage + + +class ChatChatResponse(BaseModel): + id: str + + choices: List[Choice] + + created: int + + model: str diff --git a/src/writer_ai/types/completion.py b/src/writer_ai/types/completion.py new file mode 100644 index 00000000..20adc287 --- /dev/null +++ b/src/writer_ai/types/completion.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .._models import BaseModel + +__all__ = ["Completion", "Choice", "ChoiceLogProbs", "ChoiceLogProbsTopLogProb"] + + +class ChoiceLogProbsTopLogProb(BaseModel): + additional_properties: Optional[float] = None + + +class ChoiceLogProbs(BaseModel): + text_offset: Optional[List[int]] = None + + token_log_probs: Optional[List[float]] = None + + tokens: Optional[List[str]] = None + + top_log_probs: Optional[List[ChoiceLogProbsTopLogProb]] = None + + +class Choice(BaseModel): + text: str + + log_probs: Optional[ChoiceLogProbs] = None + + +class Completion(BaseModel): + choices: List[Choice] + + model: Optional[str] = None diff --git a/src/writer_ai/types/completion_create_params.py b/src/writer_ai/types/completion_create_params.py new file mode 100644 index 00000000..c7580aa7 --- /dev/null +++ b/src/writer_ai/types/completion_create_params.py @@ -0,0 +1,37 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Union +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["CompletionCreateParamsBase", "CompletionCreateParamsNonStreaming", "CompletionCreateParamsStreaming"] + + +class CompletionCreateParamsBase(TypedDict, total=False): + model: Required[str] + + prompt: Required[str] + + best_of: int + + max_tokens: int + + random_seed: int + + stop: List[str] + + temperature: float + + top_p: float + + +class CompletionCreateParamsNonStreaming(CompletionCreateParamsBase): + stream: Literal[False] + + +class CompletionCreateParamsStreaming(CompletionCreateParamsBase): + stream: Required[Literal[True]] + + +CompletionCreateParams = Union[CompletionCreateParamsNonStreaming, CompletionCreateParamsStreaming] diff --git a/src/writer_ai/types/model_list_response.py b/src/writer_ai/types/model_list_response.py new file mode 100644 index 00000000..5ff56b69 --- /dev/null +++ b/src/writer_ai/types/model_list_response.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List + +from .._models import BaseModel + +__all__ = ["ModelListResponse", "Model"] + + +class Model(BaseModel): + id: str + + name: str + + +class ModelListResponse(BaseModel): + models: List[Model] diff --git a/src/writer_ai/types/streaming_data.py b/src/writer_ai/types/streaming_data.py new file mode 100644 index 00000000..966912bd --- /dev/null +++ b/src/writer_ai/types/streaming_data.py @@ -0,0 +1,11 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + + + +from .._models import BaseModel + +__all__ = ["StreamingData"] + + +class StreamingData(BaseModel): + value: str diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..fd8019a9 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/__init__.py b/tests/api_resources/__init__.py new file mode 100644 index 00000000..fd8019a9 --- /dev/null +++ b/tests/api_resources/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/test_chat.py b/tests/api_resources/test_chat.py new file mode 100644 index 00000000..a1ae3271 --- /dev/null +++ b/tests/api_resources/test_chat.py @@ -0,0 +1,158 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from writer_ai import WriterAI, AsyncWriterAI +from tests.utils import assert_matches_type +from writer_ai.types import ChatChatResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestChat: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_chat(self, client: WriterAI) -> None: + chat = client.chat.chat( + messages=[ + { + "content": "string", + "role": "user", + } + ], + model="string", + ) + assert_matches_type(ChatChatResponse, chat, path=["response"]) + + @parametrize + def test_method_chat_with_all_params(self, client: WriterAI) -> None: + chat = client.chat.chat( + messages=[ + { + "content": "string", + "role": "user", + "name": "string", + } + ], + model="string", + max_tokens=0, + n=0, + stop=["string", "string", "string"], + temperature=0, + top_p=0, + ) + assert_matches_type(ChatChatResponse, chat, path=["response"]) + + @parametrize + def test_raw_response_chat(self, client: WriterAI) -> None: + response = client.chat.with_raw_response.chat( + messages=[ + { + "content": "string", + "role": "user", + } + ], + model="string", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + chat = response.parse() + assert_matches_type(ChatChatResponse, chat, path=["response"]) + + @parametrize + def test_streaming_response_chat(self, client: WriterAI) -> None: + with client.chat.with_streaming_response.chat( + messages=[ + { + "content": "string", + "role": "user", + } + ], + model="string", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + chat = response.parse() + assert_matches_type(ChatChatResponse, chat, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncChat: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_chat(self, async_client: AsyncWriterAI) -> None: + chat = await async_client.chat.chat( + messages=[ + { + "content": "string", + "role": "user", + } + ], + model="string", + ) + assert_matches_type(ChatChatResponse, chat, path=["response"]) + + @parametrize + async def test_method_chat_with_all_params(self, async_client: AsyncWriterAI) -> None: + chat = await async_client.chat.chat( + messages=[ + { + "content": "string", + "role": "user", + "name": "string", + } + ], + model="string", + max_tokens=0, + n=0, + stop=["string", "string", "string"], + temperature=0, + top_p=0, + ) + assert_matches_type(ChatChatResponse, chat, path=["response"]) + + @parametrize + async def test_raw_response_chat(self, async_client: AsyncWriterAI) -> None: + response = await async_client.chat.with_raw_response.chat( + messages=[ + { + "content": "string", + "role": "user", + } + ], + model="string", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + chat = await response.parse() + assert_matches_type(ChatChatResponse, chat, path=["response"]) + + @parametrize + async def test_streaming_response_chat(self, async_client: AsyncWriterAI) -> None: + async with async_client.chat.with_streaming_response.chat( + messages=[ + { + "content": "string", + "role": "user", + } + ], + model="string", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + chat = await response.parse() + assert_matches_type(ChatChatResponse, chat, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_completions.py b/tests/api_resources/test_completions.py new file mode 100644 index 00000000..11781207 --- /dev/null +++ b/tests/api_resources/test_completions.py @@ -0,0 +1,222 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from writer_ai import WriterAI, AsyncWriterAI +from tests.utils import assert_matches_type +from writer_ai.types import Completion + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestCompletions: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create_overload_1(self, client: WriterAI) -> None: + completion = client.completions.create( + model="string", + prompt="string", + ) + assert_matches_type(Completion, completion, path=["response"]) + + @parametrize + def test_method_create_with_all_params_overload_1(self, client: WriterAI) -> None: + completion = client.completions.create( + model="string", + prompt="string", + best_of=0, + max_tokens=0, + random_seed=0, + stop=["string", "string", "string"], + stream=False, + temperature=0, + top_p=0, + ) + assert_matches_type(Completion, completion, path=["response"]) + + @parametrize + def test_raw_response_create_overload_1(self, client: WriterAI) -> None: + response = client.completions.with_raw_response.create( + model="string", + prompt="string", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + completion = response.parse() + assert_matches_type(Completion, completion, path=["response"]) + + @parametrize + def test_streaming_response_create_overload_1(self, client: WriterAI) -> None: + with client.completions.with_streaming_response.create( + model="string", + prompt="string", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + completion = response.parse() + assert_matches_type(Completion, completion, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_create_overload_2(self, client: WriterAI) -> None: + completion_stream = client.completions.create( + model="string", + prompt="string", + stream=True, + ) + completion_stream.response.close() + + @parametrize + def test_method_create_with_all_params_overload_2(self, client: WriterAI) -> None: + completion_stream = client.completions.create( + model="string", + prompt="string", + stream=True, + best_of=0, + max_tokens=0, + random_seed=0, + stop=["string", "string", "string"], + temperature=0, + top_p=0, + ) + completion_stream.response.close() + + @parametrize + def test_raw_response_create_overload_2(self, client: WriterAI) -> None: + response = client.completions.with_raw_response.create( + model="string", + prompt="string", + stream=True, + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = response.parse() + stream.close() + + @parametrize + def test_streaming_response_create_overload_2(self, client: WriterAI) -> None: + with client.completions.with_streaming_response.create( + model="string", + prompt="string", + stream=True, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = response.parse() + stream.close() + + assert cast(Any, response.is_closed) is True + + +class TestAsyncCompletions: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_create_overload_1(self, async_client: AsyncWriterAI) -> None: + completion = await async_client.completions.create( + model="string", + prompt="string", + ) + assert_matches_type(Completion, completion, path=["response"]) + + @parametrize + async def test_method_create_with_all_params_overload_1(self, async_client: AsyncWriterAI) -> None: + completion = await async_client.completions.create( + model="string", + prompt="string", + best_of=0, + max_tokens=0, + random_seed=0, + stop=["string", "string", "string"], + stream=False, + temperature=0, + top_p=0, + ) + assert_matches_type(Completion, completion, path=["response"]) + + @parametrize + async def test_raw_response_create_overload_1(self, async_client: AsyncWriterAI) -> None: + response = await async_client.completions.with_raw_response.create( + model="string", + prompt="string", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + completion = await response.parse() + assert_matches_type(Completion, completion, path=["response"]) + + @parametrize + async def test_streaming_response_create_overload_1(self, async_client: AsyncWriterAI) -> None: + async with async_client.completions.with_streaming_response.create( + model="string", + prompt="string", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + completion = await response.parse() + assert_matches_type(Completion, completion, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_create_overload_2(self, async_client: AsyncWriterAI) -> None: + completion_stream = await async_client.completions.create( + model="string", + prompt="string", + stream=True, + ) + await completion_stream.response.aclose() + + @parametrize + async def test_method_create_with_all_params_overload_2(self, async_client: AsyncWriterAI) -> None: + completion_stream = await async_client.completions.create( + model="string", + prompt="string", + stream=True, + best_of=0, + max_tokens=0, + random_seed=0, + stop=["string", "string", "string"], + temperature=0, + top_p=0, + ) + await completion_stream.response.aclose() + + @parametrize + async def test_raw_response_create_overload_2(self, async_client: AsyncWriterAI) -> None: + response = await async_client.completions.with_raw_response.create( + model="string", + prompt="string", + stream=True, + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = await response.parse() + await stream.close() + + @parametrize + async def test_streaming_response_create_overload_2(self, async_client: AsyncWriterAI) -> None: + async with async_client.completions.with_streaming_response.create( + model="string", + prompt="string", + stream=True, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = await response.parse() + await stream.close() + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_models.py b/tests/api_resources/test_models.py new file mode 100644 index 00000000..8c032fd8 --- /dev/null +++ b/tests/api_resources/test_models.py @@ -0,0 +1,72 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from writer_ai import WriterAI, AsyncWriterAI +from tests.utils import assert_matches_type +from writer_ai.types import ModelListResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestModels: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_list(self, client: WriterAI) -> None: + model = client.models.list() + assert_matches_type(ModelListResponse, model, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: WriterAI) -> None: + response = client.models.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + model = response.parse() + assert_matches_type(ModelListResponse, model, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: WriterAI) -> None: + with client.models.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + model = response.parse() + assert_matches_type(ModelListResponse, model, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncModels: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_list(self, async_client: AsyncWriterAI) -> None: + model = await async_client.models.list() + assert_matches_type(ModelListResponse, model, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncWriterAI) -> None: + response = await async_client.models.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + model = await response.parse() + assert_matches_type(ModelListResponse, model, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncWriterAI) -> None: + async with async_client.models.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + model = await response.parse() + assert_matches_type(ModelListResponse, model, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..1b0b32e6 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +import os +import asyncio +import logging +from typing import TYPE_CHECKING, Iterator, AsyncIterator + +import pytest + +from writer_ai import WriterAI, AsyncWriterAI + +if TYPE_CHECKING: + from _pytest.fixtures import FixtureRequest + +pytest.register_assert_rewrite("tests.utils") + +logging.getLogger("writer_ai").setLevel(logging.DEBUG) + + +@pytest.fixture(scope="session") +def event_loop() -> Iterator[asyncio.AbstractEventLoop]: + loop = asyncio.new_event_loop() + yield loop + loop.close() + + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + +api_key = "My API Key" + + +@pytest.fixture(scope="session") +def client(request: FixtureRequest) -> Iterator[WriterAI]: + strict = getattr(request, "param", True) + if not isinstance(strict, bool): + raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") + + with WriterAI(base_url=base_url, api_key=api_key, _strict_response_validation=strict) as client: + yield client + + +@pytest.fixture(scope="session") +async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncWriterAI]: + strict = getattr(request, "param", True) + if not isinstance(strict, bool): + raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") + + async with AsyncWriterAI(base_url=base_url, api_key=api_key, _strict_response_validation=strict) as client: + yield client diff --git a/tests/sample_file.txt b/tests/sample_file.txt new file mode 100644 index 00000000..af5626b4 --- /dev/null +++ b/tests/sample_file.txt @@ -0,0 +1 @@ +Hello, world! diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 00000000..6cfd6c75 --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,1497 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import gc +import os +import json +import asyncio +import inspect +import tracemalloc +from typing import Any, Union, cast +from unittest import mock + +import httpx +import pytest +from respx import MockRouter +from pydantic import ValidationError + +from writer_ai import WriterAI, AsyncWriterAI, APIResponseValidationError +from writer_ai._models import BaseModel, FinalRequestOptions +from writer_ai._constants import RAW_RESPONSE_HEADER +from writer_ai._streaming import Stream, AsyncStream +from writer_ai._exceptions import WriterAIError, APIStatusError, APITimeoutError, APIResponseValidationError +from writer_ai._base_client import ( + DEFAULT_TIMEOUT, + HTTPX_DEFAULT_TIMEOUT, + BaseClient, + make_request_options, +) + +from .utils import update_env + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") +api_key = "My API Key" + + +def _get_params(client: BaseClient[Any, Any]) -> dict[str, str]: + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + url = httpx.URL(request.url) + return dict(url.params) + + +def _low_retry_timeout(*_args: Any, **_kwargs: Any) -> float: + return 0.1 + + +def _get_open_connections(client: WriterAI | AsyncWriterAI) -> int: + transport = client._client._transport + assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport) + + pool = transport._pool + return len(pool._requests) + + +class TestWriterAI: + client = WriterAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) + + @pytest.mark.respx(base_url=base_url) + def test_raw_response(self, respx_mock: MockRouter) -> None: + respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = self.client.post("/foo", cast_to=httpx.Response) + assert response.status_code == 200 + assert isinstance(response, httpx.Response) + assert response.json() == {"foo": "bar"} + + @pytest.mark.respx(base_url=base_url) + def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None: + respx_mock.post("/foo").mock( + return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}') + ) + + response = self.client.post("/foo", cast_to=httpx.Response) + assert response.status_code == 200 + assert isinstance(response, httpx.Response) + assert response.json() == {"foo": "bar"} + + def test_copy(self) -> None: + copied = self.client.copy() + assert id(copied) != id(self.client) + + copied = self.client.copy(api_key="another My API Key") + assert copied.api_key == "another My API Key" + assert self.client.api_key == "My API Key" + + def test_copy_default_options(self) -> None: + # options that have a default are overridden correctly + copied = self.client.copy(max_retries=7) + assert copied.max_retries == 7 + assert self.client.max_retries == 2 + + copied2 = copied.copy(max_retries=6) + assert copied2.max_retries == 6 + assert copied.max_retries == 7 + + # timeout + assert isinstance(self.client.timeout, httpx.Timeout) + copied = self.client.copy(timeout=None) + assert copied.timeout is None + assert isinstance(self.client.timeout, httpx.Timeout) + + def test_copy_default_headers(self) -> None: + client = WriterAI( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} + ) + assert client.default_headers["X-Foo"] == "bar" + + # does not override the already given value when not specified + copied = client.copy() + assert copied.default_headers["X-Foo"] == "bar" + + # merges already given headers + copied = client.copy(default_headers={"X-Bar": "stainless"}) + assert copied.default_headers["X-Foo"] == "bar" + assert copied.default_headers["X-Bar"] == "stainless" + + # uses new values for any already given headers + copied = client.copy(default_headers={"X-Foo": "stainless"}) + assert copied.default_headers["X-Foo"] == "stainless" + + # set_default_headers + + # completely overrides already set values + copied = client.copy(set_default_headers={}) + assert copied.default_headers.get("X-Foo") is None + + copied = client.copy(set_default_headers={"X-Bar": "Robert"}) + assert copied.default_headers["X-Bar"] == "Robert" + + with pytest.raises( + ValueError, + match="`default_headers` and `set_default_headers` arguments are mutually exclusive", + ): + client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"}) + + def test_copy_default_query(self) -> None: + client = WriterAI( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"} + ) + assert _get_params(client)["foo"] == "bar" + + # does not override the already given value when not specified + copied = client.copy() + assert _get_params(copied)["foo"] == "bar" + + # merges already given params + copied = client.copy(default_query={"bar": "stainless"}) + params = _get_params(copied) + assert params["foo"] == "bar" + assert params["bar"] == "stainless" + + # uses new values for any already given headers + copied = client.copy(default_query={"foo": "stainless"}) + assert _get_params(copied)["foo"] == "stainless" + + # set_default_query + + # completely overrides already set values + copied = client.copy(set_default_query={}) + assert _get_params(copied) == {} + + copied = client.copy(set_default_query={"bar": "Robert"}) + assert _get_params(copied)["bar"] == "Robert" + + with pytest.raises( + ValueError, + # TODO: update + match="`default_query` and `set_default_query` arguments are mutually exclusive", + ): + client.copy(set_default_query={}, default_query={"foo": "Bar"}) + + def test_copy_signature(self) -> None: + # ensure the same parameters that can be passed to the client are defined in the `.copy()` method + init_signature = inspect.signature( + # mypy doesn't like that we access the `__init__` property. + self.client.__init__, # type: ignore[misc] + ) + copy_signature = inspect.signature(self.client.copy) + exclude_params = {"transport", "proxies", "_strict_response_validation"} + + for name in init_signature.parameters.keys(): + if name in exclude_params: + continue + + copy_param = copy_signature.parameters.get(name) + assert copy_param is not None, f"copy() signature is missing the {name} param" + + def test_copy_build_request(self) -> None: + options = FinalRequestOptions(method="get", url="/foo") + + def build_request(options: FinalRequestOptions) -> None: + client = self.client.copy() + client._build_request(options) + + # ensure that the machinery is warmed up before tracing starts. + build_request(options) + gc.collect() + + tracemalloc.start(1000) + + snapshot_before = tracemalloc.take_snapshot() + + ITERATIONS = 10 + for _ in range(ITERATIONS): + build_request(options) + + gc.collect() + snapshot_after = tracemalloc.take_snapshot() + + tracemalloc.stop() + + def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.StatisticDiff) -> None: + if diff.count == 0: + # Avoid false positives by considering only leaks (i.e. allocations that persist). + return + + if diff.count % ITERATIONS != 0: + # Avoid false positives by considering only leaks that appear per iteration. + return + + for frame in diff.traceback: + if any( + frame.filename.endswith(fragment) + for fragment in [ + # to_raw_response_wrapper leaks through the @functools.wraps() decorator. + # + # removing the decorator fixes the leak for reasons we don't understand. + "writer_ai/_legacy_response.py", + "writer_ai/_response.py", + # pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason. + "writer_ai/_compat.py", + # Standard library leaks we don't care about. + "/logging/__init__.py", + ] + ): + return + + leaks.append(diff) + + leaks: list[tracemalloc.StatisticDiff] = [] + for diff in snapshot_after.compare_to(snapshot_before, "traceback"): + add_leak(leaks, diff) + if leaks: + for leak in leaks: + print("MEMORY LEAK:", leak) + for frame in leak.traceback: + print(frame) + raise AssertionError() + + def test_request_timeout(self) -> None: + request = self.client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == DEFAULT_TIMEOUT + + request = self.client._build_request( + FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0)) + ) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == httpx.Timeout(100.0) + + def test_client_timeout_option(self) -> None: + client = WriterAI( + base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0) + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == httpx.Timeout(0) + + def test_http_client_timeout_option(self) -> None: + # custom timeout given to the httpx client should be used + with httpx.Client(timeout=None) as http_client: + client = WriterAI( + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == httpx.Timeout(None) + + # no timeout given to the httpx client should not use the httpx default + with httpx.Client() as http_client: + client = WriterAI( + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == DEFAULT_TIMEOUT + + # explicitly passing the default timeout currently results in it being ignored + with httpx.Client(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: + client = WriterAI( + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == DEFAULT_TIMEOUT # our default + + async def test_invalid_http_client(self) -> None: + with pytest.raises(TypeError, match="Invalid `http_client` arg"): + async with httpx.AsyncClient() as http_client: + WriterAI( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + http_client=cast(Any, http_client), + ) + + def test_default_headers_option(self) -> None: + client = WriterAI( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} + ) + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("x-foo") == "bar" + assert request.headers.get("x-stainless-lang") == "python" + + client2 = WriterAI( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + default_headers={ + "X-Foo": "stainless", + "X-Stainless-Lang": "my-overriding-header", + }, + ) + request = client2._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("x-foo") == "stainless" + assert request.headers.get("x-stainless-lang") == "my-overriding-header" + + def test_validate_headers(self) -> None: + client = WriterAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("Authorization") == f"Bearer {api_key}" + + with pytest.raises(WriterAIError): + client2 = WriterAI(base_url=base_url, api_key=None, _strict_response_validation=True) + _ = client2 + + def test_default_query_option(self) -> None: + client = WriterAI( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} + ) + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + url = httpx.URL(request.url) + assert dict(url.params) == {"query_param": "bar"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/foo", + params={"foo": "baz", "query_param": "overriden"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"foo": "baz", "query_param": "overriden"} + + def test_request_extra_json(self) -> None: + request = self.client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar"}, + extra_json={"baz": False}, + ), + ) + data = json.loads(request.content.decode("utf-8")) + assert data == {"foo": "bar", "baz": False} + + request = self.client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + extra_json={"baz": False}, + ), + ) + data = json.loads(request.content.decode("utf-8")) + assert data == {"baz": False} + + # `extra_json` takes priority over `json_data` when keys clash + request = self.client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar", "baz": True}, + extra_json={"baz": None}, + ), + ) + data = json.loads(request.content.decode("utf-8")) + assert data == {"foo": "bar", "baz": None} + + def test_request_extra_headers(self) -> None: + request = self.client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options(extra_headers={"X-Foo": "Foo"}), + ), + ) + assert request.headers.get("X-Foo") == "Foo" + + # `extra_headers` takes priority over `default_headers` when keys clash + request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + extra_headers={"X-Bar": "false"}, + ), + ), + ) + assert request.headers.get("X-Bar") == "false" + + def test_request_extra_query(self) -> None: + request = self.client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + extra_query={"my_query_param": "Foo"}, + ), + ), + ) + params = dict(request.url.params) + assert params == {"my_query_param": "Foo"} + + # if both `query` and `extra_query` are given, they are merged + request = self.client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + query={"bar": "1"}, + extra_query={"foo": "2"}, + ), + ), + ) + params = dict(request.url.params) + assert params == {"bar": "1", "foo": "2"} + + # `extra_query` takes priority over `query` when keys clash + request = self.client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + query={"foo": "1"}, + extra_query={"foo": "2"}, + ), + ), + ) + params = dict(request.url.params) + assert params == {"foo": "2"} + + def test_multipart_repeating_array(self, client: WriterAI) -> None: + request = client._build_request( + FinalRequestOptions.construct( + method="get", + url="/foo", + headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"}, + json_data={"array": ["foo", "bar"]}, + files=[("foo.txt", b"hello world")], + ) + ) + + assert request.read().split(b"\r\n") == [ + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="array[]"', + b"", + b"foo", + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="array[]"', + b"", + b"bar", + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="foo.txt"; filename="upload"', + b"Content-Type: application/octet-stream", + b"", + b"hello world", + b"--6b7ba517decee4a450543ea6ae821c82--", + b"", + ] + + @pytest.mark.respx(base_url=base_url) + def test_basic_union_response(self, respx_mock: MockRouter) -> None: + class Model1(BaseModel): + name: str + + class Model2(BaseModel): + foo: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model2) + assert response.foo == "bar" + + @pytest.mark.respx(base_url=base_url) + def test_union_response_different_types(self, respx_mock: MockRouter) -> None: + """Union of objects with the same field name using a different type""" + + class Model1(BaseModel): + foo: int + + class Model2(BaseModel): + foo: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model2) + assert response.foo == "bar" + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1})) + + response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model1) + assert response.foo == 1 + + @pytest.mark.respx(base_url=base_url) + def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None: + """ + Response that sets Content-Type to something other than application/json but returns json data + """ + + class Model(BaseModel): + foo: int + + respx_mock.get("/foo").mock( + return_value=httpx.Response( + 200, + content=json.dumps({"foo": 2}), + headers={"Content-Type": "application/text"}, + ) + ) + + response = self.client.get("/foo", cast_to=Model) + assert isinstance(response, Model) + assert response.foo == 2 + + def test_base_url_setter(self) -> None: + client = WriterAI(base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True) + assert client.base_url == "https://example.com/from_init/" + + client.base_url = "https://example.com/from_setter" # type: ignore[assignment] + + assert client.base_url == "https://example.com/from_setter/" + + def test_base_url_env(self) -> None: + with update_env(WRITER_AI_BASE_URL="http://localhost:5000/from/env"): + client = WriterAI(api_key=api_key, _strict_response_validation=True) + assert client.base_url == "http://localhost:5000/from/env/" + + @pytest.mark.parametrize( + "client", + [ + WriterAI(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), + WriterAI( + base_url="http://localhost:5000/custom/path/", + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.Client(), + ), + ], + ids=["standard", "custom http client"], + ) + def test_base_url_trailing_slash(self, client: WriterAI) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "http://localhost:5000/custom/path/foo" + + @pytest.mark.parametrize( + "client", + [ + WriterAI(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), + WriterAI( + base_url="http://localhost:5000/custom/path/", + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.Client(), + ), + ], + ids=["standard", "custom http client"], + ) + def test_base_url_no_trailing_slash(self, client: WriterAI) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "http://localhost:5000/custom/path/foo" + + @pytest.mark.parametrize( + "client", + [ + WriterAI(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), + WriterAI( + base_url="http://localhost:5000/custom/path/", + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.Client(), + ), + ], + ids=["standard", "custom http client"], + ) + def test_absolute_request_url(self, client: WriterAI) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="https://myapi.com/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "https://myapi.com/foo" + + def test_copied_client_does_not_close_http(self) -> None: + client = WriterAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) + assert not client.is_closed() + + copied = client.copy() + assert copied is not client + + del copied + + assert not client.is_closed() + + def test_client_context_manager(self) -> None: + client = WriterAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) + with client as c2: + assert c2 is client + assert not c2.is_closed() + assert not client.is_closed() + assert client.is_closed() + + @pytest.mark.respx(base_url=base_url) + def test_client_response_validation_error(self, respx_mock: MockRouter) -> None: + class Model(BaseModel): + foo: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}})) + + with pytest.raises(APIResponseValidationError) as exc: + self.client.get("/foo", cast_to=Model) + + assert isinstance(exc.value.__cause__, ValidationError) + + def test_client_max_retries_validation(self) -> None: + with pytest.raises(TypeError, match=r"max_retries cannot be None"): + WriterAI(base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None)) + + @pytest.mark.respx(base_url=base_url) + def test_default_stream_cls(self, respx_mock: MockRouter) -> None: + class Model(BaseModel): + name: str + + respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + stream = self.client.post("/foo", cast_to=Model, stream=True, stream_cls=Stream[Model]) + assert isinstance(stream, Stream) + stream.response.close() + + @pytest.mark.respx(base_url=base_url) + def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None: + class Model(BaseModel): + name: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) + + strict_client = WriterAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) + + with pytest.raises(APIResponseValidationError): + strict_client.get("/foo", cast_to=Model) + + client = WriterAI(base_url=base_url, api_key=api_key, _strict_response_validation=False) + + response = client.get("/foo", cast_to=Model) + assert isinstance(response, str) # type: ignore[unreachable] + + @pytest.mark.parametrize( + "remaining_retries,retry_after,timeout", + [ + [3, "20", 20], + [3, "0", 0.5], + [3, "-10", 0.5], + [3, "60", 60], + [3, "61", 0.5], + [3, "Fri, 29 Sep 2023 16:26:57 GMT", 20], + [3, "Fri, 29 Sep 2023 16:26:37 GMT", 0.5], + [3, "Fri, 29 Sep 2023 16:26:27 GMT", 0.5], + [3, "Fri, 29 Sep 2023 16:27:37 GMT", 60], + [3, "Fri, 29 Sep 2023 16:27:38 GMT", 0.5], + [3, "99999999999999999999999999999999999", 0.5], + [3, "Zun, 29 Sep 2023 16:26:27 GMT", 0.5], + [3, "", 0.5], + [2, "", 0.5 * 2.0], + [1, "", 0.5 * 4.0], + ], + ) + @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) + def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None: + client = WriterAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) + + headers = httpx.Headers({"retry-after": retry_after}) + options = FinalRequestOptions(method="get", url="/foo", max_retries=3) + calculated = client._calculate_retry_timeout(remaining_retries, options, headers) + assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType] + + @mock.patch("writer_ai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: + respx_mock.post("/v1/chat").mock(side_effect=httpx.TimeoutException("Test timeout error")) + + with pytest.raises(APITimeoutError): + self.client.post( + "/v1/chat", + body=cast( + object, + dict( + messages=[ + { + "content": "string", + "role": "user", + } + ], + model="string", + ), + ), + cast_to=httpx.Response, + options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, + ) + + assert _get_open_connections(self.client) == 0 + + @mock.patch("writer_ai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: + respx_mock.post("/v1/chat").mock(return_value=httpx.Response(500)) + + with pytest.raises(APIStatusError): + self.client.post( + "/v1/chat", + body=cast( + object, + dict( + messages=[ + { + "content": "string", + "role": "user", + } + ], + model="string", + ), + ), + cast_to=httpx.Response, + options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, + ) + + assert _get_open_connections(self.client) == 0 + + +class TestAsyncWriterAI: + client = AsyncWriterAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) + + @pytest.mark.respx(base_url=base_url) + @pytest.mark.asyncio + async def test_raw_response(self, respx_mock: MockRouter) -> None: + respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = await self.client.post("/foo", cast_to=httpx.Response) + assert response.status_code == 200 + assert isinstance(response, httpx.Response) + assert response.json() == {"foo": "bar"} + + @pytest.mark.respx(base_url=base_url) + @pytest.mark.asyncio + async def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None: + respx_mock.post("/foo").mock( + return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}') + ) + + response = await self.client.post("/foo", cast_to=httpx.Response) + assert response.status_code == 200 + assert isinstance(response, httpx.Response) + assert response.json() == {"foo": "bar"} + + def test_copy(self) -> None: + copied = self.client.copy() + assert id(copied) != id(self.client) + + copied = self.client.copy(api_key="another My API Key") + assert copied.api_key == "another My API Key" + assert self.client.api_key == "My API Key" + + def test_copy_default_options(self) -> None: + # options that have a default are overridden correctly + copied = self.client.copy(max_retries=7) + assert copied.max_retries == 7 + assert self.client.max_retries == 2 + + copied2 = copied.copy(max_retries=6) + assert copied2.max_retries == 6 + assert copied.max_retries == 7 + + # timeout + assert isinstance(self.client.timeout, httpx.Timeout) + copied = self.client.copy(timeout=None) + assert copied.timeout is None + assert isinstance(self.client.timeout, httpx.Timeout) + + def test_copy_default_headers(self) -> None: + client = AsyncWriterAI( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} + ) + assert client.default_headers["X-Foo"] == "bar" + + # does not override the already given value when not specified + copied = client.copy() + assert copied.default_headers["X-Foo"] == "bar" + + # merges already given headers + copied = client.copy(default_headers={"X-Bar": "stainless"}) + assert copied.default_headers["X-Foo"] == "bar" + assert copied.default_headers["X-Bar"] == "stainless" + + # uses new values for any already given headers + copied = client.copy(default_headers={"X-Foo": "stainless"}) + assert copied.default_headers["X-Foo"] == "stainless" + + # set_default_headers + + # completely overrides already set values + copied = client.copy(set_default_headers={}) + assert copied.default_headers.get("X-Foo") is None + + copied = client.copy(set_default_headers={"X-Bar": "Robert"}) + assert copied.default_headers["X-Bar"] == "Robert" + + with pytest.raises( + ValueError, + match="`default_headers` and `set_default_headers` arguments are mutually exclusive", + ): + client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"}) + + def test_copy_default_query(self) -> None: + client = AsyncWriterAI( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"} + ) + assert _get_params(client)["foo"] == "bar" + + # does not override the already given value when not specified + copied = client.copy() + assert _get_params(copied)["foo"] == "bar" + + # merges already given params + copied = client.copy(default_query={"bar": "stainless"}) + params = _get_params(copied) + assert params["foo"] == "bar" + assert params["bar"] == "stainless" + + # uses new values for any already given headers + copied = client.copy(default_query={"foo": "stainless"}) + assert _get_params(copied)["foo"] == "stainless" + + # set_default_query + + # completely overrides already set values + copied = client.copy(set_default_query={}) + assert _get_params(copied) == {} + + copied = client.copy(set_default_query={"bar": "Robert"}) + assert _get_params(copied)["bar"] == "Robert" + + with pytest.raises( + ValueError, + # TODO: update + match="`default_query` and `set_default_query` arguments are mutually exclusive", + ): + client.copy(set_default_query={}, default_query={"foo": "Bar"}) + + def test_copy_signature(self) -> None: + # ensure the same parameters that can be passed to the client are defined in the `.copy()` method + init_signature = inspect.signature( + # mypy doesn't like that we access the `__init__` property. + self.client.__init__, # type: ignore[misc] + ) + copy_signature = inspect.signature(self.client.copy) + exclude_params = {"transport", "proxies", "_strict_response_validation"} + + for name in init_signature.parameters.keys(): + if name in exclude_params: + continue + + copy_param = copy_signature.parameters.get(name) + assert copy_param is not None, f"copy() signature is missing the {name} param" + + def test_copy_build_request(self) -> None: + options = FinalRequestOptions(method="get", url="/foo") + + def build_request(options: FinalRequestOptions) -> None: + client = self.client.copy() + client._build_request(options) + + # ensure that the machinery is warmed up before tracing starts. + build_request(options) + gc.collect() + + tracemalloc.start(1000) + + snapshot_before = tracemalloc.take_snapshot() + + ITERATIONS = 10 + for _ in range(ITERATIONS): + build_request(options) + + gc.collect() + snapshot_after = tracemalloc.take_snapshot() + + tracemalloc.stop() + + def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.StatisticDiff) -> None: + if diff.count == 0: + # Avoid false positives by considering only leaks (i.e. allocations that persist). + return + + if diff.count % ITERATIONS != 0: + # Avoid false positives by considering only leaks that appear per iteration. + return + + for frame in diff.traceback: + if any( + frame.filename.endswith(fragment) + for fragment in [ + # to_raw_response_wrapper leaks through the @functools.wraps() decorator. + # + # removing the decorator fixes the leak for reasons we don't understand. + "writer_ai/_legacy_response.py", + "writer_ai/_response.py", + # pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason. + "writer_ai/_compat.py", + # Standard library leaks we don't care about. + "/logging/__init__.py", + ] + ): + return + + leaks.append(diff) + + leaks: list[tracemalloc.StatisticDiff] = [] + for diff in snapshot_after.compare_to(snapshot_before, "traceback"): + add_leak(leaks, diff) + if leaks: + for leak in leaks: + print("MEMORY LEAK:", leak) + for frame in leak.traceback: + print(frame) + raise AssertionError() + + async def test_request_timeout(self) -> None: + request = self.client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == DEFAULT_TIMEOUT + + request = self.client._build_request( + FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0)) + ) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == httpx.Timeout(100.0) + + async def test_client_timeout_option(self) -> None: + client = AsyncWriterAI( + base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0) + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == httpx.Timeout(0) + + async def test_http_client_timeout_option(self) -> None: + # custom timeout given to the httpx client should be used + async with httpx.AsyncClient(timeout=None) as http_client: + client = AsyncWriterAI( + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == httpx.Timeout(None) + + # no timeout given to the httpx client should not use the httpx default + async with httpx.AsyncClient() as http_client: + client = AsyncWriterAI( + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == DEFAULT_TIMEOUT + + # explicitly passing the default timeout currently results in it being ignored + async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: + client = AsyncWriterAI( + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == DEFAULT_TIMEOUT # our default + + def test_invalid_http_client(self) -> None: + with pytest.raises(TypeError, match="Invalid `http_client` arg"): + with httpx.Client() as http_client: + AsyncWriterAI( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + http_client=cast(Any, http_client), + ) + + def test_default_headers_option(self) -> None: + client = AsyncWriterAI( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} + ) + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("x-foo") == "bar" + assert request.headers.get("x-stainless-lang") == "python" + + client2 = AsyncWriterAI( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + default_headers={ + "X-Foo": "stainless", + "X-Stainless-Lang": "my-overriding-header", + }, + ) + request = client2._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("x-foo") == "stainless" + assert request.headers.get("x-stainless-lang") == "my-overriding-header" + + def test_validate_headers(self) -> None: + client = AsyncWriterAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("Authorization") == f"Bearer {api_key}" + + with pytest.raises(WriterAIError): + client2 = AsyncWriterAI(base_url=base_url, api_key=None, _strict_response_validation=True) + _ = client2 + + def test_default_query_option(self) -> None: + client = AsyncWriterAI( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} + ) + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + url = httpx.URL(request.url) + assert dict(url.params) == {"query_param": "bar"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/foo", + params={"foo": "baz", "query_param": "overriden"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"foo": "baz", "query_param": "overriden"} + + def test_request_extra_json(self) -> None: + request = self.client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar"}, + extra_json={"baz": False}, + ), + ) + data = json.loads(request.content.decode("utf-8")) + assert data == {"foo": "bar", "baz": False} + + request = self.client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + extra_json={"baz": False}, + ), + ) + data = json.loads(request.content.decode("utf-8")) + assert data == {"baz": False} + + # `extra_json` takes priority over `json_data` when keys clash + request = self.client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar", "baz": True}, + extra_json={"baz": None}, + ), + ) + data = json.loads(request.content.decode("utf-8")) + assert data == {"foo": "bar", "baz": None} + + def test_request_extra_headers(self) -> None: + request = self.client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options(extra_headers={"X-Foo": "Foo"}), + ), + ) + assert request.headers.get("X-Foo") == "Foo" + + # `extra_headers` takes priority over `default_headers` when keys clash + request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + extra_headers={"X-Bar": "false"}, + ), + ), + ) + assert request.headers.get("X-Bar") == "false" + + def test_request_extra_query(self) -> None: + request = self.client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + extra_query={"my_query_param": "Foo"}, + ), + ), + ) + params = dict(request.url.params) + assert params == {"my_query_param": "Foo"} + + # if both `query` and `extra_query` are given, they are merged + request = self.client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + query={"bar": "1"}, + extra_query={"foo": "2"}, + ), + ), + ) + params = dict(request.url.params) + assert params == {"bar": "1", "foo": "2"} + + # `extra_query` takes priority over `query` when keys clash + request = self.client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + query={"foo": "1"}, + extra_query={"foo": "2"}, + ), + ), + ) + params = dict(request.url.params) + assert params == {"foo": "2"} + + def test_multipart_repeating_array(self, async_client: AsyncWriterAI) -> None: + request = async_client._build_request( + FinalRequestOptions.construct( + method="get", + url="/foo", + headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"}, + json_data={"array": ["foo", "bar"]}, + files=[("foo.txt", b"hello world")], + ) + ) + + assert request.read().split(b"\r\n") == [ + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="array[]"', + b"", + b"foo", + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="array[]"', + b"", + b"bar", + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="foo.txt"; filename="upload"', + b"Content-Type: application/octet-stream", + b"", + b"hello world", + b"--6b7ba517decee4a450543ea6ae821c82--", + b"", + ] + + @pytest.mark.respx(base_url=base_url) + async def test_basic_union_response(self, respx_mock: MockRouter) -> None: + class Model1(BaseModel): + name: str + + class Model2(BaseModel): + foo: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model2) + assert response.foo == "bar" + + @pytest.mark.respx(base_url=base_url) + async def test_union_response_different_types(self, respx_mock: MockRouter) -> None: + """Union of objects with the same field name using a different type""" + + class Model1(BaseModel): + foo: int + + class Model2(BaseModel): + foo: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model2) + assert response.foo == "bar" + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1})) + + response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model1) + assert response.foo == 1 + + @pytest.mark.respx(base_url=base_url) + async def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None: + """ + Response that sets Content-Type to something other than application/json but returns json data + """ + + class Model(BaseModel): + foo: int + + respx_mock.get("/foo").mock( + return_value=httpx.Response( + 200, + content=json.dumps({"foo": 2}), + headers={"Content-Type": "application/text"}, + ) + ) + + response = await self.client.get("/foo", cast_to=Model) + assert isinstance(response, Model) + assert response.foo == 2 + + def test_base_url_setter(self) -> None: + client = AsyncWriterAI( + base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True + ) + assert client.base_url == "https://example.com/from_init/" + + client.base_url = "https://example.com/from_setter" # type: ignore[assignment] + + assert client.base_url == "https://example.com/from_setter/" + + def test_base_url_env(self) -> None: + with update_env(WRITER_AI_BASE_URL="http://localhost:5000/from/env"): + client = AsyncWriterAI(api_key=api_key, _strict_response_validation=True) + assert client.base_url == "http://localhost:5000/from/env/" + + @pytest.mark.parametrize( + "client", + [ + AsyncWriterAI( + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True + ), + AsyncWriterAI( + base_url="http://localhost:5000/custom/path/", + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.AsyncClient(), + ), + ], + ids=["standard", "custom http client"], + ) + def test_base_url_trailing_slash(self, client: AsyncWriterAI) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "http://localhost:5000/custom/path/foo" + + @pytest.mark.parametrize( + "client", + [ + AsyncWriterAI( + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True + ), + AsyncWriterAI( + base_url="http://localhost:5000/custom/path/", + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.AsyncClient(), + ), + ], + ids=["standard", "custom http client"], + ) + def test_base_url_no_trailing_slash(self, client: AsyncWriterAI) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "http://localhost:5000/custom/path/foo" + + @pytest.mark.parametrize( + "client", + [ + AsyncWriterAI( + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True + ), + AsyncWriterAI( + base_url="http://localhost:5000/custom/path/", + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.AsyncClient(), + ), + ], + ids=["standard", "custom http client"], + ) + def test_absolute_request_url(self, client: AsyncWriterAI) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="https://myapi.com/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "https://myapi.com/foo" + + async def test_copied_client_does_not_close_http(self) -> None: + client = AsyncWriterAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) + assert not client.is_closed() + + copied = client.copy() + assert copied is not client + + del copied + + await asyncio.sleep(0.2) + assert not client.is_closed() + + async def test_client_context_manager(self) -> None: + client = AsyncWriterAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) + async with client as c2: + assert c2 is client + assert not c2.is_closed() + assert not client.is_closed() + assert client.is_closed() + + @pytest.mark.respx(base_url=base_url) + @pytest.mark.asyncio + async def test_client_response_validation_error(self, respx_mock: MockRouter) -> None: + class Model(BaseModel): + foo: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}})) + + with pytest.raises(APIResponseValidationError) as exc: + await self.client.get("/foo", cast_to=Model) + + assert isinstance(exc.value.__cause__, ValidationError) + + async def test_client_max_retries_validation(self) -> None: + with pytest.raises(TypeError, match=r"max_retries cannot be None"): + AsyncWriterAI( + base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None) + ) + + @pytest.mark.respx(base_url=base_url) + @pytest.mark.asyncio + async def test_default_stream_cls(self, respx_mock: MockRouter) -> None: + class Model(BaseModel): + name: str + + respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + stream = await self.client.post("/foo", cast_to=Model, stream=True, stream_cls=AsyncStream[Model]) + assert isinstance(stream, AsyncStream) + await stream.response.aclose() + + @pytest.mark.respx(base_url=base_url) + @pytest.mark.asyncio + async def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None: + class Model(BaseModel): + name: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) + + strict_client = AsyncWriterAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) + + with pytest.raises(APIResponseValidationError): + await strict_client.get("/foo", cast_to=Model) + + client = AsyncWriterAI(base_url=base_url, api_key=api_key, _strict_response_validation=False) + + response = await client.get("/foo", cast_to=Model) + assert isinstance(response, str) # type: ignore[unreachable] + + @pytest.mark.parametrize( + "remaining_retries,retry_after,timeout", + [ + [3, "20", 20], + [3, "0", 0.5], + [3, "-10", 0.5], + [3, "60", 60], + [3, "61", 0.5], + [3, "Fri, 29 Sep 2023 16:26:57 GMT", 20], + [3, "Fri, 29 Sep 2023 16:26:37 GMT", 0.5], + [3, "Fri, 29 Sep 2023 16:26:27 GMT", 0.5], + [3, "Fri, 29 Sep 2023 16:27:37 GMT", 60], + [3, "Fri, 29 Sep 2023 16:27:38 GMT", 0.5], + [3, "99999999999999999999999999999999999", 0.5], + [3, "Zun, 29 Sep 2023 16:26:27 GMT", 0.5], + [3, "", 0.5], + [2, "", 0.5 * 2.0], + [1, "", 0.5 * 4.0], + ], + ) + @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) + @pytest.mark.asyncio + async def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None: + client = AsyncWriterAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) + + headers = httpx.Headers({"retry-after": retry_after}) + options = FinalRequestOptions(method="get", url="/foo", max_retries=3) + calculated = client._calculate_retry_timeout(remaining_retries, options, headers) + assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType] + + @mock.patch("writer_ai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: + respx_mock.post("/v1/chat").mock(side_effect=httpx.TimeoutException("Test timeout error")) + + with pytest.raises(APITimeoutError): + await self.client.post( + "/v1/chat", + body=cast( + object, + dict( + messages=[ + { + "content": "string", + "role": "user", + } + ], + model="string", + ), + ), + cast_to=httpx.Response, + options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, + ) + + assert _get_open_connections(self.client) == 0 + + @mock.patch("writer_ai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: + respx_mock.post("/v1/chat").mock(return_value=httpx.Response(500)) + + with pytest.raises(APIStatusError): + await self.client.post( + "/v1/chat", + body=cast( + object, + dict( + messages=[ + { + "content": "string", + "role": "user", + } + ], + model="string", + ), + ), + cast_to=httpx.Response, + options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, + ) + + assert _get_open_connections(self.client) == 0 diff --git a/tests/test_deepcopy.py b/tests/test_deepcopy.py new file mode 100644 index 00000000..c6608761 --- /dev/null +++ b/tests/test_deepcopy.py @@ -0,0 +1,59 @@ +from writer_ai._utils import deepcopy_minimal + + +def assert_different_identities(obj1: object, obj2: object) -> None: + assert obj1 == obj2 + assert id(obj1) != id(obj2) + + +def test_simple_dict() -> None: + obj1 = {"foo": "bar"} + obj2 = deepcopy_minimal(obj1) + assert_different_identities(obj1, obj2) + + +def test_nested_dict() -> None: + obj1 = {"foo": {"bar": True}} + obj2 = deepcopy_minimal(obj1) + assert_different_identities(obj1, obj2) + assert_different_identities(obj1["foo"], obj2["foo"]) + + +def test_complex_nested_dict() -> None: + obj1 = {"foo": {"bar": [{"hello": "world"}]}} + obj2 = deepcopy_minimal(obj1) + assert_different_identities(obj1, obj2) + assert_different_identities(obj1["foo"], obj2["foo"]) + assert_different_identities(obj1["foo"]["bar"], obj2["foo"]["bar"]) + assert_different_identities(obj1["foo"]["bar"][0], obj2["foo"]["bar"][0]) + + +def test_simple_list() -> None: + obj1 = ["a", "b", "c"] + obj2 = deepcopy_minimal(obj1) + assert_different_identities(obj1, obj2) + + +def test_nested_list() -> None: + obj1 = ["a", [1, 2, 3]] + obj2 = deepcopy_minimal(obj1) + assert_different_identities(obj1, obj2) + assert_different_identities(obj1[1], obj2[1]) + + +class MyObject: + ... + + +def test_ignores_other_types() -> None: + # custom classes + my_obj = MyObject() + obj1 = {"foo": my_obj} + obj2 = deepcopy_minimal(obj1) + assert_different_identities(obj1, obj2) + assert obj1["foo"] is my_obj + + # tuples + obj3 = ("a", "b") + obj4 = deepcopy_minimal(obj3) + assert obj3 is obj4 diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py new file mode 100644 index 00000000..55d0fcd3 --- /dev/null +++ b/tests/test_extract_files.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +from typing import Sequence + +import pytest + +from writer_ai._types import FileTypes +from writer_ai._utils import extract_files + + +def test_removes_files_from_input() -> None: + query = {"foo": "bar"} + assert extract_files(query, paths=[]) == [] + assert query == {"foo": "bar"} + + query2 = {"foo": b"Bar", "hello": "world"} + assert extract_files(query2, paths=[["foo"]]) == [("foo", b"Bar")] + assert query2 == {"hello": "world"} + + query3 = {"foo": {"foo": {"bar": b"Bar"}}, "hello": "world"} + assert extract_files(query3, paths=[["foo", "foo", "bar"]]) == [("foo[foo][bar]", b"Bar")] + assert query3 == {"foo": {"foo": {}}, "hello": "world"} + + query4 = {"foo": {"bar": b"Bar", "baz": "foo"}, "hello": "world"} + assert extract_files(query4, paths=[["foo", "bar"]]) == [("foo[bar]", b"Bar")] + assert query4 == {"hello": "world", "foo": {"baz": "foo"}} + + +def test_multiple_files() -> None: + query = {"documents": [{"file": b"My first file"}, {"file": b"My second file"}]} + assert extract_files(query, paths=[["documents", "", "file"]]) == [ + ("documents[][file]", b"My first file"), + ("documents[][file]", b"My second file"), + ] + assert query == {"documents": [{}, {}]} + + +@pytest.mark.parametrize( + "query,paths,expected", + [ + [ + {"foo": {"bar": "baz"}}, + [["foo", "", "bar"]], + [], + ], + [ + {"foo": ["bar", "baz"]}, + [["foo", "bar"]], + [], + ], + [ + {"foo": {"bar": "baz"}}, + [["foo", "foo"]], + [], + ], + ], + ids=["dict expecting array", "array expecting dict", "unknown keys"], +) +def test_ignores_incorrect_paths( + query: dict[str, object], + paths: Sequence[Sequence[str]], + expected: list[tuple[str, FileTypes]], +) -> None: + assert extract_files(query, paths=paths) == expected diff --git a/tests/test_files.py b/tests/test_files.py new file mode 100644 index 00000000..cc8247bf --- /dev/null +++ b/tests/test_files.py @@ -0,0 +1,51 @@ +from pathlib import Path + +import anyio +import pytest +from dirty_equals import IsDict, IsList, IsBytes, IsTuple + +from writer_ai._files import to_httpx_files, async_to_httpx_files + +readme_path = Path(__file__).parent.parent.joinpath("README.md") + + +def test_pathlib_includes_file_name() -> None: + result = to_httpx_files({"file": readme_path}) + print(result) + assert result == IsDict({"file": IsTuple("README.md", IsBytes())}) + + +def test_tuple_input() -> None: + result = to_httpx_files([("file", readme_path)]) + print(result) + assert result == IsList(IsTuple("file", IsTuple("README.md", IsBytes()))) + + +@pytest.mark.asyncio +async def test_async_pathlib_includes_file_name() -> None: + result = await async_to_httpx_files({"file": readme_path}) + print(result) + assert result == IsDict({"file": IsTuple("README.md", IsBytes())}) + + +@pytest.mark.asyncio +async def test_async_supports_anyio_path() -> None: + result = await async_to_httpx_files({"file": anyio.Path(readme_path)}) + print(result) + assert result == IsDict({"file": IsTuple("README.md", IsBytes())}) + + +@pytest.mark.asyncio +async def test_async_tuple_input() -> None: + result = await async_to_httpx_files([("file", readme_path)]) + print(result) + assert result == IsList(IsTuple("file", IsTuple("README.md", IsBytes()))) + + +def test_string_not_allowed() -> None: + with pytest.raises(TypeError, match="Expected file types input to be a FileContent type or to be a tuple"): + to_httpx_files( + { + "file": "foo", # type: ignore + } + ) diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 00000000..e0f8cee4 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,829 @@ +import json +from typing import Any, Dict, List, Union, Optional, cast +from datetime import datetime, timezone +from typing_extensions import Literal, Annotated + +import pytest +import pydantic +from pydantic import Field + +from writer_ai._utils import PropertyInfo +from writer_ai._compat import PYDANTIC_V2, parse_obj, model_dump, model_json +from writer_ai._models import BaseModel, construct_type + + +class BasicModel(BaseModel): + foo: str + + +@pytest.mark.parametrize("value", ["hello", 1], ids=["correct type", "mismatched"]) +def test_basic(value: object) -> None: + m = BasicModel.construct(foo=value) + assert m.foo == value + + +def test_directly_nested_model() -> None: + class NestedModel(BaseModel): + nested: BasicModel + + m = NestedModel.construct(nested={"foo": "Foo!"}) + assert m.nested.foo == "Foo!" + + # mismatched types + m = NestedModel.construct(nested="hello!") + assert m.nested == "hello!" + + +def test_optional_nested_model() -> None: + class NestedModel(BaseModel): + nested: Optional[BasicModel] + + m1 = NestedModel.construct(nested=None) + assert m1.nested is None + + m2 = NestedModel.construct(nested={"foo": "bar"}) + assert m2.nested is not None + assert m2.nested.foo == "bar" + + # mismatched types + m3 = NestedModel.construct(nested={"foo"}) + assert isinstance(cast(Any, m3.nested), set) + assert m3.nested == {"foo"} + + +def test_list_nested_model() -> None: + class NestedModel(BaseModel): + nested: List[BasicModel] + + m = NestedModel.construct(nested=[{"foo": "bar"}, {"foo": "2"}]) + assert m.nested is not None + assert isinstance(m.nested, list) + assert len(m.nested) == 2 + assert m.nested[0].foo == "bar" + assert m.nested[1].foo == "2" + + # mismatched types + m = NestedModel.construct(nested=True) + assert cast(Any, m.nested) is True + + m = NestedModel.construct(nested=[False]) + assert cast(Any, m.nested) == [False] + + +def test_optional_list_nested_model() -> None: + class NestedModel(BaseModel): + nested: Optional[List[BasicModel]] + + m1 = NestedModel.construct(nested=[{"foo": "bar"}, {"foo": "2"}]) + assert m1.nested is not None + assert isinstance(m1.nested, list) + assert len(m1.nested) == 2 + assert m1.nested[0].foo == "bar" + assert m1.nested[1].foo == "2" + + m2 = NestedModel.construct(nested=None) + assert m2.nested is None + + # mismatched types + m3 = NestedModel.construct(nested={1}) + assert cast(Any, m3.nested) == {1} + + m4 = NestedModel.construct(nested=[False]) + assert cast(Any, m4.nested) == [False] + + +def test_list_optional_items_nested_model() -> None: + class NestedModel(BaseModel): + nested: List[Optional[BasicModel]] + + m = NestedModel.construct(nested=[None, {"foo": "bar"}]) + assert m.nested is not None + assert isinstance(m.nested, list) + assert len(m.nested) == 2 + assert m.nested[0] is None + assert m.nested[1] is not None + assert m.nested[1].foo == "bar" + + # mismatched types + m3 = NestedModel.construct(nested="foo") + assert cast(Any, m3.nested) == "foo" + + m4 = NestedModel.construct(nested=[False]) + assert cast(Any, m4.nested) == [False] + + +def test_list_mismatched_type() -> None: + class NestedModel(BaseModel): + nested: List[str] + + m = NestedModel.construct(nested=False) + assert cast(Any, m.nested) is False + + +def test_raw_dictionary() -> None: + class NestedModel(BaseModel): + nested: Dict[str, str] + + m = NestedModel.construct(nested={"hello": "world"}) + assert m.nested == {"hello": "world"} + + # mismatched types + m = NestedModel.construct(nested=False) + assert cast(Any, m.nested) is False + + +def test_nested_dictionary_model() -> None: + class NestedModel(BaseModel): + nested: Dict[str, BasicModel] + + m = NestedModel.construct(nested={"hello": {"foo": "bar"}}) + assert isinstance(m.nested, dict) + assert m.nested["hello"].foo == "bar" + + # mismatched types + m = NestedModel.construct(nested={"hello": False}) + assert cast(Any, m.nested["hello"]) is False + + +def test_unknown_fields() -> None: + m1 = BasicModel.construct(foo="foo", unknown=1) + assert m1.foo == "foo" + assert cast(Any, m1).unknown == 1 + + m2 = BasicModel.construct(foo="foo", unknown={"foo_bar": True}) + assert m2.foo == "foo" + assert cast(Any, m2).unknown == {"foo_bar": True} + + assert model_dump(m2) == {"foo": "foo", "unknown": {"foo_bar": True}} + + +def test_strict_validation_unknown_fields() -> None: + class Model(BaseModel): + foo: str + + model = parse_obj(Model, dict(foo="hello!", user="Robert")) + assert model.foo == "hello!" + assert cast(Any, model).user == "Robert" + + assert model_dump(model) == {"foo": "hello!", "user": "Robert"} + + +def test_aliases() -> None: + class Model(BaseModel): + my_field: int = Field(alias="myField") + + m = Model.construct(myField=1) + assert m.my_field == 1 + + # mismatched types + m = Model.construct(myField={"hello": False}) + assert cast(Any, m.my_field) == {"hello": False} + + +def test_repr() -> None: + model = BasicModel(foo="bar") + assert str(model) == "BasicModel(foo='bar')" + assert repr(model) == "BasicModel(foo='bar')" + + +def test_repr_nested_model() -> None: + class Child(BaseModel): + name: str + age: int + + class Parent(BaseModel): + name: str + child: Child + + model = Parent(name="Robert", child=Child(name="Foo", age=5)) + assert str(model) == "Parent(name='Robert', child=Child(name='Foo', age=5))" + assert repr(model) == "Parent(name='Robert', child=Child(name='Foo', age=5))" + + +def test_optional_list() -> None: + class Submodel(BaseModel): + name: str + + class Model(BaseModel): + items: Optional[List[Submodel]] + + m = Model.construct(items=None) + assert m.items is None + + m = Model.construct(items=[]) + assert m.items == [] + + m = Model.construct(items=[{"name": "Robert"}]) + assert m.items is not None + assert len(m.items) == 1 + assert m.items[0].name == "Robert" + + +def test_nested_union_of_models() -> None: + class Submodel1(BaseModel): + bar: bool + + class Submodel2(BaseModel): + thing: str + + class Model(BaseModel): + foo: Union[Submodel1, Submodel2] + + m = Model.construct(foo={"thing": "hello"}) + assert isinstance(m.foo, Submodel2) + assert m.foo.thing == "hello" + + +def test_nested_union_of_mixed_types() -> None: + class Submodel1(BaseModel): + bar: bool + + class Model(BaseModel): + foo: Union[Submodel1, Literal[True], Literal["CARD_HOLDER"]] + + m = Model.construct(foo=True) + assert m.foo is True + + m = Model.construct(foo="CARD_HOLDER") + assert m.foo is "CARD_HOLDER" + + m = Model.construct(foo={"bar": False}) + assert isinstance(m.foo, Submodel1) + assert m.foo.bar is False + + +def test_nested_union_multiple_variants() -> None: + class Submodel1(BaseModel): + bar: bool + + class Submodel2(BaseModel): + thing: str + + class Submodel3(BaseModel): + foo: int + + class Model(BaseModel): + foo: Union[Submodel1, Submodel2, None, Submodel3] + + m = Model.construct(foo={"thing": "hello"}) + assert isinstance(m.foo, Submodel2) + assert m.foo.thing == "hello" + + m = Model.construct(foo=None) + assert m.foo is None + + m = Model.construct() + assert m.foo is None + + m = Model.construct(foo={"foo": "1"}) + assert isinstance(m.foo, Submodel3) + assert m.foo.foo == 1 + + +def test_nested_union_invalid_data() -> None: + class Submodel1(BaseModel): + level: int + + class Submodel2(BaseModel): + name: str + + class Model(BaseModel): + foo: Union[Submodel1, Submodel2] + + m = Model.construct(foo=True) + assert cast(bool, m.foo) is True + + m = Model.construct(foo={"name": 3}) + if PYDANTIC_V2: + assert isinstance(m.foo, Submodel1) + assert m.foo.name == 3 # type: ignore + else: + assert isinstance(m.foo, Submodel2) + assert m.foo.name == "3" + + +def test_list_of_unions() -> None: + class Submodel1(BaseModel): + level: int + + class Submodel2(BaseModel): + name: str + + class Model(BaseModel): + items: List[Union[Submodel1, Submodel2]] + + m = Model.construct(items=[{"level": 1}, {"name": "Robert"}]) + assert len(m.items) == 2 + assert isinstance(m.items[0], Submodel1) + assert m.items[0].level == 1 + assert isinstance(m.items[1], Submodel2) + assert m.items[1].name == "Robert" + + m = Model.construct(items=[{"level": -1}, 156]) + assert len(m.items) == 2 + assert isinstance(m.items[0], Submodel1) + assert m.items[0].level == -1 + assert m.items[1] == 156 + + +def test_union_of_lists() -> None: + class SubModel1(BaseModel): + level: int + + class SubModel2(BaseModel): + name: str + + class Model(BaseModel): + items: Union[List[SubModel1], List[SubModel2]] + + # with one valid entry + m = Model.construct(items=[{"name": "Robert"}]) + assert len(m.items) == 1 + assert isinstance(m.items[0], SubModel2) + assert m.items[0].name == "Robert" + + # with two entries pointing to different types + m = Model.construct(items=[{"level": 1}, {"name": "Robert"}]) + assert len(m.items) == 2 + assert isinstance(m.items[0], SubModel1) + assert m.items[0].level == 1 + assert isinstance(m.items[1], SubModel1) + assert cast(Any, m.items[1]).name == "Robert" + + # with two entries pointing to *completely* different types + m = Model.construct(items=[{"level": -1}, 156]) + assert len(m.items) == 2 + assert isinstance(m.items[0], SubModel1) + assert m.items[0].level == -1 + assert m.items[1] == 156 + + +def test_dict_of_union() -> None: + class SubModel1(BaseModel): + name: str + + class SubModel2(BaseModel): + foo: str + + class Model(BaseModel): + data: Dict[str, Union[SubModel1, SubModel2]] + + m = Model.construct(data={"hello": {"name": "there"}, "foo": {"foo": "bar"}}) + assert len(list(m.data.keys())) == 2 + assert isinstance(m.data["hello"], SubModel1) + assert m.data["hello"].name == "there" + assert isinstance(m.data["foo"], SubModel2) + assert m.data["foo"].foo == "bar" + + # TODO: test mismatched type + + +def test_double_nested_union() -> None: + class SubModel1(BaseModel): + name: str + + class SubModel2(BaseModel): + bar: str + + class Model(BaseModel): + data: Dict[str, List[Union[SubModel1, SubModel2]]] + + m = Model.construct(data={"foo": [{"bar": "baz"}, {"name": "Robert"}]}) + assert len(m.data["foo"]) == 2 + + entry1 = m.data["foo"][0] + assert isinstance(entry1, SubModel2) + assert entry1.bar == "baz" + + entry2 = m.data["foo"][1] + assert isinstance(entry2, SubModel1) + assert entry2.name == "Robert" + + # TODO: test mismatched type + + +def test_union_of_dict() -> None: + class SubModel1(BaseModel): + name: str + + class SubModel2(BaseModel): + foo: str + + class Model(BaseModel): + data: Union[Dict[str, SubModel1], Dict[str, SubModel2]] + + m = Model.construct(data={"hello": {"name": "there"}, "foo": {"foo": "bar"}}) + assert len(list(m.data.keys())) == 2 + assert isinstance(m.data["hello"], SubModel1) + assert m.data["hello"].name == "there" + assert isinstance(m.data["foo"], SubModel1) + assert cast(Any, m.data["foo"]).foo == "bar" + + +def test_iso8601_datetime() -> None: + class Model(BaseModel): + created_at: datetime + + expected = datetime(2019, 12, 27, 18, 11, 19, 117000, tzinfo=timezone.utc) + + if PYDANTIC_V2: + expected_json = '{"created_at":"2019-12-27T18:11:19.117000Z"}' + else: + expected_json = '{"created_at": "2019-12-27T18:11:19.117000+00:00"}' + + model = Model.construct(created_at="2019-12-27T18:11:19.117Z") + assert model.created_at == expected + assert model_json(model) == expected_json + + model = parse_obj(Model, dict(created_at="2019-12-27T18:11:19.117Z")) + assert model.created_at == expected + assert model_json(model) == expected_json + + +def test_does_not_coerce_int() -> None: + class Model(BaseModel): + bar: int + + assert Model.construct(bar=1).bar == 1 + assert Model.construct(bar=10.9).bar == 10.9 + assert Model.construct(bar="19").bar == "19" # type: ignore[comparison-overlap] + assert Model.construct(bar=False).bar is False + + +def test_int_to_float_safe_conversion() -> None: + class Model(BaseModel): + float_field: float + + m = Model.construct(float_field=10) + assert m.float_field == 10.0 + assert isinstance(m.float_field, float) + + m = Model.construct(float_field=10.12) + assert m.float_field == 10.12 + assert isinstance(m.float_field, float) + + # number too big + m = Model.construct(float_field=2**53 + 1) + assert m.float_field == 2**53 + 1 + assert isinstance(m.float_field, int) + + +def test_deprecated_alias() -> None: + class Model(BaseModel): + resource_id: str = Field(alias="model_id") + + @property + def model_id(self) -> str: + return self.resource_id + + m = Model.construct(model_id="id") + assert m.model_id == "id" + assert m.resource_id == "id" + assert m.resource_id is m.model_id + + m = parse_obj(Model, {"model_id": "id"}) + assert m.model_id == "id" + assert m.resource_id == "id" + assert m.resource_id is m.model_id + + +def test_omitted_fields() -> None: + class Model(BaseModel): + resource_id: Optional[str] = None + + m = Model.construct() + assert "resource_id" not in m.model_fields_set + + m = Model.construct(resource_id=None) + assert "resource_id" in m.model_fields_set + + m = Model.construct(resource_id="foo") + assert "resource_id" in m.model_fields_set + + +def test_to_dict() -> None: + class Model(BaseModel): + foo: Optional[str] = Field(alias="FOO", default=None) + + m = Model(FOO="hello") + assert m.to_dict() == {"FOO": "hello"} + assert m.to_dict(use_api_names=False) == {"foo": "hello"} + + m2 = Model() + assert m2.to_dict() == {} + assert m2.to_dict(exclude_unset=False) == {"FOO": None} + assert m2.to_dict(exclude_unset=False, exclude_none=True) == {} + assert m2.to_dict(exclude_unset=False, exclude_defaults=True) == {} + + m3 = Model(FOO=None) + assert m3.to_dict() == {"FOO": None} + assert m3.to_dict(exclude_none=True) == {} + assert m3.to_dict(exclude_defaults=True) == {} + + if PYDANTIC_V2: + + class Model2(BaseModel): + created_at: datetime + + time_str = "2024-03-21T11:39:01.275859" + m4 = Model2.construct(created_at=time_str) + assert m4.to_dict(mode="python") == {"created_at": datetime.fromisoformat(time_str)} + assert m4.to_dict(mode="json") == {"created_at": time_str} + else: + with pytest.raises(ValueError, match="mode is only supported in Pydantic v2"): + m.to_dict(mode="json") + + with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): + m.to_dict(warnings=False) + + +def test_forwards_compat_model_dump_method() -> None: + class Model(BaseModel): + foo: Optional[str] = Field(alias="FOO", default=None) + + m = Model(FOO="hello") + assert m.model_dump() == {"foo": "hello"} + assert m.model_dump(include={"bar"}) == {} + assert m.model_dump(exclude={"foo"}) == {} + assert m.model_dump(by_alias=True) == {"FOO": "hello"} + + m2 = Model() + assert m2.model_dump() == {"foo": None} + assert m2.model_dump(exclude_unset=True) == {} + assert m2.model_dump(exclude_none=True) == {} + assert m2.model_dump(exclude_defaults=True) == {} + + m3 = Model(FOO=None) + assert m3.model_dump() == {"foo": None} + assert m3.model_dump(exclude_none=True) == {} + + if not PYDANTIC_V2: + with pytest.raises(ValueError, match="mode is only supported in Pydantic v2"): + m.model_dump(mode="json") + + with pytest.raises(ValueError, match="round_trip is only supported in Pydantic v2"): + m.model_dump(round_trip=True) + + with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): + m.model_dump(warnings=False) + + +def test_to_json() -> None: + class Model(BaseModel): + foo: Optional[str] = Field(alias="FOO", default=None) + + m = Model(FOO="hello") + assert json.loads(m.to_json()) == {"FOO": "hello"} + assert json.loads(m.to_json(use_api_names=False)) == {"foo": "hello"} + + if PYDANTIC_V2: + assert m.to_json(indent=None) == '{"FOO":"hello"}' + else: + assert m.to_json(indent=None) == '{"FOO": "hello"}' + + m2 = Model() + assert json.loads(m2.to_json()) == {} + assert json.loads(m2.to_json(exclude_unset=False)) == {"FOO": None} + assert json.loads(m2.to_json(exclude_unset=False, exclude_none=True)) == {} + assert json.loads(m2.to_json(exclude_unset=False, exclude_defaults=True)) == {} + + m3 = Model(FOO=None) + assert json.loads(m3.to_json()) == {"FOO": None} + assert json.loads(m3.to_json(exclude_none=True)) == {} + + if not PYDANTIC_V2: + with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): + m.to_json(warnings=False) + + +def test_forwards_compat_model_dump_json_method() -> None: + class Model(BaseModel): + foo: Optional[str] = Field(alias="FOO", default=None) + + m = Model(FOO="hello") + assert json.loads(m.model_dump_json()) == {"foo": "hello"} + assert json.loads(m.model_dump_json(include={"bar"})) == {} + assert json.loads(m.model_dump_json(include={"foo"})) == {"foo": "hello"} + assert json.loads(m.model_dump_json(by_alias=True)) == {"FOO": "hello"} + + assert m.model_dump_json(indent=2) == '{\n "foo": "hello"\n}' + + m2 = Model() + assert json.loads(m2.model_dump_json()) == {"foo": None} + assert json.loads(m2.model_dump_json(exclude_unset=True)) == {} + assert json.loads(m2.model_dump_json(exclude_none=True)) == {} + assert json.loads(m2.model_dump_json(exclude_defaults=True)) == {} + + m3 = Model(FOO=None) + assert json.loads(m3.model_dump_json()) == {"foo": None} + assert json.loads(m3.model_dump_json(exclude_none=True)) == {} + + if not PYDANTIC_V2: + with pytest.raises(ValueError, match="round_trip is only supported in Pydantic v2"): + m.model_dump_json(round_trip=True) + + with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): + m.model_dump_json(warnings=False) + + +def test_type_compat() -> None: + # our model type can be assigned to Pydantic's model type + + def takes_pydantic(model: pydantic.BaseModel) -> None: # noqa: ARG001 + ... + + class OurModel(BaseModel): + foo: Optional[str] = None + + takes_pydantic(OurModel()) + + +def test_annotated_types() -> None: + class Model(BaseModel): + value: str + + m = construct_type( + value={"value": "foo"}, + type_=cast(Any, Annotated[Model, "random metadata"]), + ) + assert isinstance(m, Model) + assert m.value == "foo" + + +def test_discriminated_unions_invalid_data() -> None: + class A(BaseModel): + type: Literal["a"] + + data: str + + class B(BaseModel): + type: Literal["b"] + + data: int + + m = construct_type( + value={"type": "b", "data": "foo"}, + type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="type")]), + ) + assert isinstance(m, B) + assert m.type == "b" + assert m.data == "foo" # type: ignore[comparison-overlap] + + m = construct_type( + value={"type": "a", "data": 100}, + type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="type")]), + ) + assert isinstance(m, A) + assert m.type == "a" + if PYDANTIC_V2: + assert m.data == 100 # type: ignore[comparison-overlap] + else: + # pydantic v1 automatically converts inputs to strings + # if the expected type is a str + assert m.data == "100" + + +def test_discriminated_unions_unknown_variant() -> None: + class A(BaseModel): + type: Literal["a"] + + data: str + + class B(BaseModel): + type: Literal["b"] + + data: int + + m = construct_type( + value={"type": "c", "data": None, "new_thing": "bar"}, + type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="type")]), + ) + + # just chooses the first variant + assert isinstance(m, A) + assert m.type == "c" # type: ignore[comparison-overlap] + assert m.data == None # type: ignore[unreachable] + assert m.new_thing == "bar" + + +def test_discriminated_unions_invalid_data_nested_unions() -> None: + class A(BaseModel): + type: Literal["a"] + + data: str + + class B(BaseModel): + type: Literal["b"] + + data: int + + class C(BaseModel): + type: Literal["c"] + + data: bool + + m = construct_type( + value={"type": "b", "data": "foo"}, + type_=cast(Any, Annotated[Union[Union[A, B], C], PropertyInfo(discriminator="type")]), + ) + assert isinstance(m, B) + assert m.type == "b" + assert m.data == "foo" # type: ignore[comparison-overlap] + + m = construct_type( + value={"type": "c", "data": "foo"}, + type_=cast(Any, Annotated[Union[Union[A, B], C], PropertyInfo(discriminator="type")]), + ) + assert isinstance(m, C) + assert m.type == "c" + assert m.data == "foo" # type: ignore[comparison-overlap] + + +def test_discriminated_unions_with_aliases_invalid_data() -> None: + class A(BaseModel): + foo_type: Literal["a"] = Field(alias="type") + + data: str + + class B(BaseModel): + foo_type: Literal["b"] = Field(alias="type") + + data: int + + m = construct_type( + value={"type": "b", "data": "foo"}, + type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="foo_type")]), + ) + assert isinstance(m, B) + assert m.foo_type == "b" + assert m.data == "foo" # type: ignore[comparison-overlap] + + m = construct_type( + value={"type": "a", "data": 100}, + type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="foo_type")]), + ) + assert isinstance(m, A) + assert m.foo_type == "a" + if PYDANTIC_V2: + assert m.data == 100 # type: ignore[comparison-overlap] + else: + # pydantic v1 automatically converts inputs to strings + # if the expected type is a str + assert m.data == "100" + + +def test_discriminated_unions_overlapping_discriminators_invalid_data() -> None: + class A(BaseModel): + type: Literal["a"] + + data: bool + + class B(BaseModel): + type: Literal["a"] + + data: int + + m = construct_type( + value={"type": "a", "data": "foo"}, + type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="type")]), + ) + assert isinstance(m, B) + assert m.type == "a" + assert m.data == "foo" # type: ignore[comparison-overlap] + + +def test_discriminated_unions_invalid_data_uses_cache() -> None: + class A(BaseModel): + type: Literal["a"] + + data: str + + class B(BaseModel): + type: Literal["b"] + + data: int + + UnionType = cast(Any, Union[A, B]) + + assert not hasattr(UnionType, "__discriminator__") + + m = construct_type( + value={"type": "b", "data": "foo"}, type_=cast(Any, Annotated[UnionType, PropertyInfo(discriminator="type")]) + ) + assert isinstance(m, B) + assert m.type == "b" + assert m.data == "foo" # type: ignore[comparison-overlap] + + discriminator = UnionType.__discriminator__ + assert discriminator is not None + + m = construct_type( + value={"type": "b", "data": "foo"}, type_=cast(Any, Annotated[UnionType, PropertyInfo(discriminator="type")]) + ) + assert isinstance(m, B) + assert m.type == "b" + assert m.data == "foo" # type: ignore[comparison-overlap] + + # if the discriminator details object stays the same between invocations then + # we hit the cache + assert UnionType.__discriminator__ is discriminator diff --git a/tests/test_qs.py b/tests/test_qs.py new file mode 100644 index 00000000..e1d4f943 --- /dev/null +++ b/tests/test_qs.py @@ -0,0 +1,78 @@ +from typing import Any, cast +from functools import partial +from urllib.parse import unquote + +import pytest + +from writer_ai._qs import Querystring, stringify + + +def test_empty() -> None: + assert stringify({}) == "" + assert stringify({"a": {}}) == "" + assert stringify({"a": {"b": {"c": {}}}}) == "" + + +def test_basic() -> None: + assert stringify({"a": 1}) == "a=1" + assert stringify({"a": "b"}) == "a=b" + assert stringify({"a": True}) == "a=true" + assert stringify({"a": False}) == "a=false" + assert stringify({"a": 1.23456}) == "a=1.23456" + assert stringify({"a": None}) == "" + + +@pytest.mark.parametrize("method", ["class", "function"]) +def test_nested_dotted(method: str) -> None: + if method == "class": + serialise = Querystring(nested_format="dots").stringify + else: + serialise = partial(stringify, nested_format="dots") + + assert unquote(serialise({"a": {"b": "c"}})) == "a.b=c" + assert unquote(serialise({"a": {"b": "c", "d": "e", "f": "g"}})) == "a.b=c&a.d=e&a.f=g" + assert unquote(serialise({"a": {"b": {"c": {"d": "e"}}}})) == "a.b.c.d=e" + assert unquote(serialise({"a": {"b": True}})) == "a.b=true" + + +def test_nested_brackets() -> None: + assert unquote(stringify({"a": {"b": "c"}})) == "a[b]=c" + assert unquote(stringify({"a": {"b": "c", "d": "e", "f": "g"}})) == "a[b]=c&a[d]=e&a[f]=g" + assert unquote(stringify({"a": {"b": {"c": {"d": "e"}}}})) == "a[b][c][d]=e" + assert unquote(stringify({"a": {"b": True}})) == "a[b]=true" + + +@pytest.mark.parametrize("method", ["class", "function"]) +def test_array_comma(method: str) -> None: + if method == "class": + serialise = Querystring(array_format="comma").stringify + else: + serialise = partial(stringify, array_format="comma") + + assert unquote(serialise({"in": ["foo", "bar"]})) == "in=foo,bar" + assert unquote(serialise({"a": {"b": [True, False]}})) == "a[b]=true,false" + assert unquote(serialise({"a": {"b": [True, False, None, True]}})) == "a[b]=true,false,true" + + +def test_array_repeat() -> None: + assert unquote(stringify({"in": ["foo", "bar"]})) == "in=foo&in=bar" + assert unquote(stringify({"a": {"b": [True, False]}})) == "a[b]=true&a[b]=false" + assert unquote(stringify({"a": {"b": [True, False, None, True]}})) == "a[b]=true&a[b]=false&a[b]=true" + assert unquote(stringify({"in": ["foo", {"b": {"c": ["d", "e"]}}]})) == "in=foo&in[b][c]=d&in[b][c]=e" + + +@pytest.mark.parametrize("method", ["class", "function"]) +def test_array_brackets(method: str) -> None: + if method == "class": + serialise = Querystring(array_format="brackets").stringify + else: + serialise = partial(stringify, array_format="brackets") + + assert unquote(serialise({"in": ["foo", "bar"]})) == "in[]=foo&in[]=bar" + assert unquote(serialise({"a": {"b": [True, False]}})) == "a[b][]=true&a[b][]=false" + assert unquote(serialise({"a": {"b": [True, False, None, True]}})) == "a[b][]=true&a[b][]=false&a[b][]=true" + + +def test_unknown_array_format() -> None: + with pytest.raises(NotImplementedError, match="Unknown array_format value: foo, choose from comma, repeat"): + stringify({"a": ["foo", "bar"]}, array_format=cast(Any, "foo")) diff --git a/tests/test_required_args.py b/tests/test_required_args.py new file mode 100644 index 00000000..f3cf767a --- /dev/null +++ b/tests/test_required_args.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +import pytest + +from writer_ai._utils import required_args + + +def test_too_many_positional_params() -> None: + @required_args(["a"]) + def foo(a: str | None = None) -> str | None: + return a + + with pytest.raises(TypeError, match=r"foo\(\) takes 1 argument\(s\) but 2 were given"): + foo("a", "b") # type: ignore + + +def test_positional_param() -> None: + @required_args(["a"]) + def foo(a: str | None = None) -> str | None: + return a + + assert foo("a") == "a" + assert foo(None) is None + assert foo(a="b") == "b" + + with pytest.raises(TypeError, match="Missing required argument: 'a'"): + foo() + + +def test_keyword_only_param() -> None: + @required_args(["a"]) + def foo(*, a: str | None = None) -> str | None: + return a + + assert foo(a="a") == "a" + assert foo(a=None) is None + assert foo(a="b") == "b" + + with pytest.raises(TypeError, match="Missing required argument: 'a'"): + foo() + + +def test_multiple_params() -> None: + @required_args(["a", "b", "c"]) + def foo(a: str = "", *, b: str = "", c: str = "") -> str | None: + return f"{a} {b} {c}" + + assert foo(a="a", b="b", c="c") == "a b c" + + error_message = r"Missing required arguments.*" + + with pytest.raises(TypeError, match=error_message): + foo() + + with pytest.raises(TypeError, match=error_message): + foo(a="a") + + with pytest.raises(TypeError, match=error_message): + foo(b="b") + + with pytest.raises(TypeError, match=error_message): + foo(c="c") + + with pytest.raises(TypeError, match=r"Missing required argument: 'a'"): + foo(b="a", c="c") + + with pytest.raises(TypeError, match=r"Missing required argument: 'b'"): + foo("a", c="c") + + +def test_multiple_variants() -> None: + @required_args(["a"], ["b"]) + def foo(*, a: str | None = None, b: str | None = None) -> str | None: + return a if a is not None else b + + assert foo(a="foo") == "foo" + assert foo(b="bar") == "bar" + assert foo(a=None) is None + assert foo(b=None) is None + + # TODO: this error message could probably be improved + with pytest.raises( + TypeError, + match=r"Missing required arguments; Expected either \('a'\) or \('b'\) arguments to be given", + ): + foo() + + +def test_multiple_params_multiple_variants() -> None: + @required_args(["a", "b"], ["c"]) + def foo(*, a: str | None = None, b: str | None = None, c: str | None = None) -> str | None: + if a is not None: + return a + if b is not None: + return b + return c + + error_message = r"Missing required arguments; Expected either \('a' and 'b'\) or \('c'\) arguments to be given" + + with pytest.raises(TypeError, match=error_message): + foo(a="foo") + + with pytest.raises(TypeError, match=error_message): + foo(b="bar") + + with pytest.raises(TypeError, match=error_message): + foo() + + assert foo(a=None, b="bar") == "bar" + assert foo(c=None) is None + assert foo(c="foo") == "foo" diff --git a/tests/test_response.py b/tests/test_response.py new file mode 100644 index 00000000..d26a4c93 --- /dev/null +++ b/tests/test_response.py @@ -0,0 +1,194 @@ +import json +from typing import List, cast +from typing_extensions import Annotated + +import httpx +import pytest +import pydantic + +from writer_ai import WriterAI, BaseModel, AsyncWriterAI +from writer_ai._response import ( + APIResponse, + BaseAPIResponse, + AsyncAPIResponse, + BinaryAPIResponse, + AsyncBinaryAPIResponse, + extract_response_type, +) +from writer_ai._streaming import Stream +from writer_ai._base_client import FinalRequestOptions + + +class ConcreteBaseAPIResponse(APIResponse[bytes]): + ... + + +class ConcreteAPIResponse(APIResponse[List[str]]): + ... + + +class ConcreteAsyncAPIResponse(APIResponse[httpx.Response]): + ... + + +def test_extract_response_type_direct_classes() -> None: + assert extract_response_type(BaseAPIResponse[str]) == str + assert extract_response_type(APIResponse[str]) == str + assert extract_response_type(AsyncAPIResponse[str]) == str + + +def test_extract_response_type_direct_class_missing_type_arg() -> None: + with pytest.raises( + RuntimeError, + match="Expected type to have a type argument at index 0 but it did not", + ): + extract_response_type(AsyncAPIResponse) + + +def test_extract_response_type_concrete_subclasses() -> None: + assert extract_response_type(ConcreteBaseAPIResponse) == bytes + assert extract_response_type(ConcreteAPIResponse) == List[str] + assert extract_response_type(ConcreteAsyncAPIResponse) == httpx.Response + + +def test_extract_response_type_binary_response() -> None: + assert extract_response_type(BinaryAPIResponse) == bytes + assert extract_response_type(AsyncBinaryAPIResponse) == bytes + + +class PydanticModel(pydantic.BaseModel): + ... + + +def test_response_parse_mismatched_basemodel(client: WriterAI) -> None: + response = APIResponse( + raw=httpx.Response(200, content=b"foo"), + client=client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + with pytest.raises( + TypeError, + match="Pydantic models must subclass our base model type, e.g. `from writer_ai import BaseModel`", + ): + response.parse(to=PydanticModel) + + +@pytest.mark.asyncio +async def test_async_response_parse_mismatched_basemodel(async_client: AsyncWriterAI) -> None: + response = AsyncAPIResponse( + raw=httpx.Response(200, content=b"foo"), + client=async_client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + with pytest.raises( + TypeError, + match="Pydantic models must subclass our base model type, e.g. `from writer_ai import BaseModel`", + ): + await response.parse(to=PydanticModel) + + +def test_response_parse_custom_stream(client: WriterAI) -> None: + response = APIResponse( + raw=httpx.Response(200, content=b"foo"), + client=client, + stream=True, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + stream = response.parse(to=Stream[int]) + assert stream._cast_to == int + + +@pytest.mark.asyncio +async def test_async_response_parse_custom_stream(async_client: AsyncWriterAI) -> None: + response = AsyncAPIResponse( + raw=httpx.Response(200, content=b"foo"), + client=async_client, + stream=True, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + stream = await response.parse(to=Stream[int]) + assert stream._cast_to == int + + +class CustomModel(BaseModel): + foo: str + bar: int + + +def test_response_parse_custom_model(client: WriterAI) -> None: + response = APIResponse( + raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), + client=client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + obj = response.parse(to=CustomModel) + assert obj.foo == "hello!" + assert obj.bar == 2 + + +@pytest.mark.asyncio +async def test_async_response_parse_custom_model(async_client: AsyncWriterAI) -> None: + response = AsyncAPIResponse( + raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), + client=async_client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + obj = await response.parse(to=CustomModel) + assert obj.foo == "hello!" + assert obj.bar == 2 + + +def test_response_parse_annotated_type(client: WriterAI) -> None: + response = APIResponse( + raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), + client=client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + obj = response.parse( + to=cast("type[CustomModel]", Annotated[CustomModel, "random metadata"]), + ) + assert obj.foo == "hello!" + assert obj.bar == 2 + + +async def test_async_response_parse_annotated_type(async_client: AsyncWriterAI) -> None: + response = AsyncAPIResponse( + raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), + client=async_client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + obj = await response.parse( + to=cast("type[CustomModel]", Annotated[CustomModel, "random metadata"]), + ) + assert obj.foo == "hello!" + assert obj.bar == 2 diff --git a/tests/test_streaming.py b/tests/test_streaming.py new file mode 100644 index 00000000..05855a23 --- /dev/null +++ b/tests/test_streaming.py @@ -0,0 +1,248 @@ +from __future__ import annotations + +from typing import Iterator, AsyncIterator + +import httpx +import pytest + +from writer_ai import WriterAI, AsyncWriterAI +from writer_ai._streaming import Stream, AsyncStream, ServerSentEvent + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_basic(sync: bool, client: WriterAI, async_client: AsyncWriterAI) -> None: + def body() -> Iterator[bytes]: + yield b"event: completion\n" + yield b'data: {"foo":true}\n' + yield b"\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event == "completion" + assert sse.json() == {"foo": True} + + await assert_empty_iter(iterator) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_data_missing_event(sync: bool, client: WriterAI, async_client: AsyncWriterAI) -> None: + def body() -> Iterator[bytes]: + yield b'data: {"foo":true}\n' + yield b"\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event is None + assert sse.json() == {"foo": True} + + await assert_empty_iter(iterator) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_event_missing_data(sync: bool, client: WriterAI, async_client: AsyncWriterAI) -> None: + def body() -> Iterator[bytes]: + yield b"event: ping\n" + yield b"\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event == "ping" + assert sse.data == "" + + await assert_empty_iter(iterator) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_multiple_events(sync: bool, client: WriterAI, async_client: AsyncWriterAI) -> None: + def body() -> Iterator[bytes]: + yield b"event: ping\n" + yield b"\n" + yield b"event: completion\n" + yield b"\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event == "ping" + assert sse.data == "" + + sse = await iter_next(iterator) + assert sse.event == "completion" + assert sse.data == "" + + await assert_empty_iter(iterator) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_multiple_events_with_data(sync: bool, client: WriterAI, async_client: AsyncWriterAI) -> None: + def body() -> Iterator[bytes]: + yield b"event: ping\n" + yield b'data: {"foo":true}\n' + yield b"\n" + yield b"event: completion\n" + yield b'data: {"bar":false}\n' + yield b"\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event == "ping" + assert sse.json() == {"foo": True} + + sse = await iter_next(iterator) + assert sse.event == "completion" + assert sse.json() == {"bar": False} + + await assert_empty_iter(iterator) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_multiple_data_lines_with_empty_line(sync: bool, client: WriterAI, async_client: AsyncWriterAI) -> None: + def body() -> Iterator[bytes]: + yield b"event: ping\n" + yield b"data: {\n" + yield b'data: "foo":\n' + yield b"data: \n" + yield b"data:\n" + yield b"data: true}\n" + yield b"\n\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event == "ping" + assert sse.json() == {"foo": True} + assert sse.data == '{\n"foo":\n\n\ntrue}' + + await assert_empty_iter(iterator) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_data_json_escaped_double_new_line(sync: bool, client: WriterAI, async_client: AsyncWriterAI) -> None: + def body() -> Iterator[bytes]: + yield b"event: ping\n" + yield b'data: {"foo": "my long\\n\\ncontent"}' + yield b"\n\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event == "ping" + assert sse.json() == {"foo": "my long\n\ncontent"} + + await assert_empty_iter(iterator) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_multiple_data_lines(sync: bool, client: WriterAI, async_client: AsyncWriterAI) -> None: + def body() -> Iterator[bytes]: + yield b"event: ping\n" + yield b"data: {\n" + yield b'data: "foo":\n' + yield b"data: true}\n" + yield b"\n\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event == "ping" + assert sse.json() == {"foo": True} + + await assert_empty_iter(iterator) + + +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_special_new_line_character( + sync: bool, + client: WriterAI, + async_client: AsyncWriterAI, +) -> None: + def body() -> Iterator[bytes]: + yield b'data: {"content":" culpa"}\n' + yield b"\n" + yield b'data: {"content":" \xe2\x80\xa8"}\n' + yield b"\n" + yield b'data: {"content":"foo"}\n' + yield b"\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event is None + assert sse.json() == {"content": " culpa"} + + sse = await iter_next(iterator) + assert sse.event is None + assert sse.json() == {"content": " 
"} + + sse = await iter_next(iterator) + assert sse.event is None + assert sse.json() == {"content": "foo"} + + await assert_empty_iter(iterator) + + +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_multi_byte_character_multiple_chunks( + sync: bool, + client: WriterAI, + async_client: AsyncWriterAI, +) -> None: + def body() -> Iterator[bytes]: + yield b'data: {"content":"' + # bytes taken from the string 'известни' and arbitrarily split + # so that some multi-byte characters span multiple chunks + yield b"\xd0" + yield b"\xb8\xd0\xb7\xd0" + yield b"\xb2\xd0\xb5\xd1\x81\xd1\x82\xd0\xbd\xd0\xb8" + yield b'"}\n' + yield b"\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event is None + assert sse.json() == {"content": "известни"} + + +async def to_aiter(iter: Iterator[bytes]) -> AsyncIterator[bytes]: + for chunk in iter: + yield chunk + + +async def iter_next(iter: Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]) -> ServerSentEvent: + if isinstance(iter, AsyncIterator): + return await iter.__anext__() + + return next(iter) + + +async def assert_empty_iter(iter: Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]) -> None: + with pytest.raises((StopAsyncIteration, RuntimeError)): + await iter_next(iter) + + +def make_event_iterator( + content: Iterator[bytes], + *, + sync: bool, + client: WriterAI, + async_client: AsyncWriterAI, +) -> Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]: + if sync: + return Stream(cast_to=object, client=client, response=httpx.Response(200, content=content))._iter_events() + + return AsyncStream( + cast_to=object, client=async_client, response=httpx.Response(200, content=to_aiter(content)) + )._iter_events() diff --git a/tests/test_transform.py b/tests/test_transform.py new file mode 100644 index 00000000..0ff64a7e --- /dev/null +++ b/tests/test_transform.py @@ -0,0 +1,408 @@ +from __future__ import annotations + +import io +import pathlib +from typing import Any, List, Union, TypeVar, Iterable, Optional, cast +from datetime import date, datetime +from typing_extensions import Required, Annotated, TypedDict + +import pytest + +from writer_ai._types import Base64FileInput +from writer_ai._utils import ( + PropertyInfo, + transform as _transform, + parse_datetime, + async_transform as _async_transform, +) +from writer_ai._compat import PYDANTIC_V2 +from writer_ai._models import BaseModel + +_T = TypeVar("_T") + +SAMPLE_FILE_PATH = pathlib.Path(__file__).parent.joinpath("sample_file.txt") + + +async def transform( + data: _T, + expected_type: object, + use_async: bool, +) -> _T: + if use_async: + return await _async_transform(data, expected_type=expected_type) + + return _transform(data, expected_type=expected_type) + + +parametrize = pytest.mark.parametrize("use_async", [False, True], ids=["sync", "async"]) + + +class Foo1(TypedDict): + foo_bar: Annotated[str, PropertyInfo(alias="fooBar")] + + +@parametrize +@pytest.mark.asyncio +async def test_top_level_alias(use_async: bool) -> None: + assert await transform({"foo_bar": "hello"}, expected_type=Foo1, use_async=use_async) == {"fooBar": "hello"} + + +class Foo2(TypedDict): + bar: Bar2 + + +class Bar2(TypedDict): + this_thing: Annotated[int, PropertyInfo(alias="this__thing")] + baz: Annotated[Baz2, PropertyInfo(alias="Baz")] + + +class Baz2(TypedDict): + my_baz: Annotated[str, PropertyInfo(alias="myBaz")] + + +@parametrize +@pytest.mark.asyncio +async def test_recursive_typeddict(use_async: bool) -> None: + assert await transform({"bar": {"this_thing": 1}}, Foo2, use_async) == {"bar": {"this__thing": 1}} + assert await transform({"bar": {"baz": {"my_baz": "foo"}}}, Foo2, use_async) == {"bar": {"Baz": {"myBaz": "foo"}}} + + +class Foo3(TypedDict): + things: List[Bar3] + + +class Bar3(TypedDict): + my_field: Annotated[str, PropertyInfo(alias="myField")] + + +@parametrize +@pytest.mark.asyncio +async def test_list_of_typeddict(use_async: bool) -> None: + result = await transform({"things": [{"my_field": "foo"}, {"my_field": "foo2"}]}, Foo3, use_async) + assert result == {"things": [{"myField": "foo"}, {"myField": "foo2"}]} + + +class Foo4(TypedDict): + foo: Union[Bar4, Baz4] + + +class Bar4(TypedDict): + foo_bar: Annotated[str, PropertyInfo(alias="fooBar")] + + +class Baz4(TypedDict): + foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")] + + +@parametrize +@pytest.mark.asyncio +async def test_union_of_typeddict(use_async: bool) -> None: + assert await transform({"foo": {"foo_bar": "bar"}}, Foo4, use_async) == {"foo": {"fooBar": "bar"}} + assert await transform({"foo": {"foo_baz": "baz"}}, Foo4, use_async) == {"foo": {"fooBaz": "baz"}} + assert await transform({"foo": {"foo_baz": "baz", "foo_bar": "bar"}}, Foo4, use_async) == { + "foo": {"fooBaz": "baz", "fooBar": "bar"} + } + + +class Foo5(TypedDict): + foo: Annotated[Union[Bar4, List[Baz4]], PropertyInfo(alias="FOO")] + + +class Bar5(TypedDict): + foo_bar: Annotated[str, PropertyInfo(alias="fooBar")] + + +class Baz5(TypedDict): + foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")] + + +@parametrize +@pytest.mark.asyncio +async def test_union_of_list(use_async: bool) -> None: + assert await transform({"foo": {"foo_bar": "bar"}}, Foo5, use_async) == {"FOO": {"fooBar": "bar"}} + assert await transform( + { + "foo": [ + {"foo_baz": "baz"}, + {"foo_baz": "baz"}, + ] + }, + Foo5, + use_async, + ) == {"FOO": [{"fooBaz": "baz"}, {"fooBaz": "baz"}]} + + +class Foo6(TypedDict): + bar: Annotated[str, PropertyInfo(alias="Bar")] + + +@parametrize +@pytest.mark.asyncio +async def test_includes_unknown_keys(use_async: bool) -> None: + assert await transform({"bar": "bar", "baz_": {"FOO": 1}}, Foo6, use_async) == { + "Bar": "bar", + "baz_": {"FOO": 1}, + } + + +class Foo7(TypedDict): + bar: Annotated[List[Bar7], PropertyInfo(alias="bAr")] + foo: Bar7 + + +class Bar7(TypedDict): + foo: str + + +@parametrize +@pytest.mark.asyncio +async def test_ignores_invalid_input(use_async: bool) -> None: + assert await transform({"bar": ""}, Foo7, use_async) == {"bAr": ""} + assert await transform({"foo": ""}, Foo7, use_async) == {"foo": ""} + + +class DatetimeDict(TypedDict, total=False): + foo: Annotated[datetime, PropertyInfo(format="iso8601")] + + bar: Annotated[Optional[datetime], PropertyInfo(format="iso8601")] + + required: Required[Annotated[Optional[datetime], PropertyInfo(format="iso8601")]] + + list_: Required[Annotated[Optional[List[datetime]], PropertyInfo(format="iso8601")]] + + union: Annotated[Union[int, datetime], PropertyInfo(format="iso8601")] + + +class DateDict(TypedDict, total=False): + foo: Annotated[date, PropertyInfo(format="iso8601")] + + +@parametrize +@pytest.mark.asyncio +async def test_iso8601_format(use_async: bool) -> None: + dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") + assert await transform({"foo": dt}, DatetimeDict, use_async) == {"foo": "2023-02-23T14:16:36.337692+00:00"} # type: ignore[comparison-overlap] + + dt = dt.replace(tzinfo=None) + assert await transform({"foo": dt}, DatetimeDict, use_async) == {"foo": "2023-02-23T14:16:36.337692"} # type: ignore[comparison-overlap] + + assert await transform({"foo": None}, DateDict, use_async) == {"foo": None} # type: ignore[comparison-overlap] + assert await transform({"foo": date.fromisoformat("2023-02-23")}, DateDict, use_async) == {"foo": "2023-02-23"} # type: ignore[comparison-overlap] + + +@parametrize +@pytest.mark.asyncio +async def test_optional_iso8601_format(use_async: bool) -> None: + dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") + assert await transform({"bar": dt}, DatetimeDict, use_async) == {"bar": "2023-02-23T14:16:36.337692+00:00"} # type: ignore[comparison-overlap] + + assert await transform({"bar": None}, DatetimeDict, use_async) == {"bar": None} + + +@parametrize +@pytest.mark.asyncio +async def test_required_iso8601_format(use_async: bool) -> None: + dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") + assert await transform({"required": dt}, DatetimeDict, use_async) == { + "required": "2023-02-23T14:16:36.337692+00:00" + } # type: ignore[comparison-overlap] + + assert await transform({"required": None}, DatetimeDict, use_async) == {"required": None} + + +@parametrize +@pytest.mark.asyncio +async def test_union_datetime(use_async: bool) -> None: + dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") + assert await transform({"union": dt}, DatetimeDict, use_async) == { # type: ignore[comparison-overlap] + "union": "2023-02-23T14:16:36.337692+00:00" + } + + assert await transform({"union": "foo"}, DatetimeDict, use_async) == {"union": "foo"} + + +@parametrize +@pytest.mark.asyncio +async def test_nested_list_iso6801_format(use_async: bool) -> None: + dt1 = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") + dt2 = parse_datetime("2022-01-15T06:34:23Z") + assert await transform({"list_": [dt1, dt2]}, DatetimeDict, use_async) == { # type: ignore[comparison-overlap] + "list_": ["2023-02-23T14:16:36.337692+00:00", "2022-01-15T06:34:23+00:00"] + } + + +@parametrize +@pytest.mark.asyncio +async def test_datetime_custom_format(use_async: bool) -> None: + dt = parse_datetime("2022-01-15T06:34:23Z") + + result = await transform(dt, Annotated[datetime, PropertyInfo(format="custom", format_template="%H")], use_async) + assert result == "06" # type: ignore[comparison-overlap] + + +class DateDictWithRequiredAlias(TypedDict, total=False): + required_prop: Required[Annotated[date, PropertyInfo(format="iso8601", alias="prop")]] + + +@parametrize +@pytest.mark.asyncio +async def test_datetime_with_alias(use_async: bool) -> None: + assert await transform({"required_prop": None}, DateDictWithRequiredAlias, use_async) == {"prop": None} # type: ignore[comparison-overlap] + assert await transform( + {"required_prop": date.fromisoformat("2023-02-23")}, DateDictWithRequiredAlias, use_async + ) == {"prop": "2023-02-23"} # type: ignore[comparison-overlap] + + +class MyModel(BaseModel): + foo: str + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_model_to_dictionary(use_async: bool) -> None: + assert await transform(MyModel(foo="hi!"), Any, use_async) == {"foo": "hi!"} + assert await transform(MyModel.construct(foo="hi!"), Any, use_async) == {"foo": "hi!"} + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_empty_model(use_async: bool) -> None: + assert await transform(MyModel.construct(), Any, use_async) == {} + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_unknown_field(use_async: bool) -> None: + assert await transform(MyModel.construct(my_untyped_field=True), Any, use_async) == {"my_untyped_field": True} + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_mismatched_types(use_async: bool) -> None: + model = MyModel.construct(foo=True) + if PYDANTIC_V2: + with pytest.warns(UserWarning): + params = await transform(model, Any, use_async) + else: + params = await transform(model, Any, use_async) + assert params == {"foo": True} + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_mismatched_object_type(use_async: bool) -> None: + model = MyModel.construct(foo=MyModel.construct(hello="world")) + if PYDANTIC_V2: + with pytest.warns(UserWarning): + params = await transform(model, Any, use_async) + else: + params = await transform(model, Any, use_async) + assert params == {"foo": {"hello": "world"}} + + +class ModelNestedObjects(BaseModel): + nested: MyModel + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_nested_objects(use_async: bool) -> None: + model = ModelNestedObjects.construct(nested={"foo": "stainless"}) + assert isinstance(model.nested, MyModel) + assert await transform(model, Any, use_async) == {"nested": {"foo": "stainless"}} + + +class ModelWithDefaultField(BaseModel): + foo: str + with_none_default: Union[str, None] = None + with_str_default: str = "foo" + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_default_field(use_async: bool) -> None: + # should be excluded when defaults are used + model = ModelWithDefaultField.construct() + assert model.with_none_default is None + assert model.with_str_default == "foo" + assert await transform(model, Any, use_async) == {} + + # should be included when the default value is explicitly given + model = ModelWithDefaultField.construct(with_none_default=None, with_str_default="foo") + assert model.with_none_default is None + assert model.with_str_default == "foo" + assert await transform(model, Any, use_async) == {"with_none_default": None, "with_str_default": "foo"} + + # should be included when a non-default value is explicitly given + model = ModelWithDefaultField.construct(with_none_default="bar", with_str_default="baz") + assert model.with_none_default == "bar" + assert model.with_str_default == "baz" + assert await transform(model, Any, use_async) == {"with_none_default": "bar", "with_str_default": "baz"} + + +class TypedDictIterableUnion(TypedDict): + foo: Annotated[Union[Bar8, Iterable[Baz8]], PropertyInfo(alias="FOO")] + + +class Bar8(TypedDict): + foo_bar: Annotated[str, PropertyInfo(alias="fooBar")] + + +class Baz8(TypedDict): + foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")] + + +@parametrize +@pytest.mark.asyncio +async def test_iterable_of_dictionaries(use_async: bool) -> None: + assert await transform({"foo": [{"foo_baz": "bar"}]}, TypedDictIterableUnion, use_async) == { + "FOO": [{"fooBaz": "bar"}] + } + assert cast(Any, await transform({"foo": ({"foo_baz": "bar"},)}, TypedDictIterableUnion, use_async)) == { + "FOO": [{"fooBaz": "bar"}] + } + + def my_iter() -> Iterable[Baz8]: + yield {"foo_baz": "hello"} + yield {"foo_baz": "world"} + + assert await transform({"foo": my_iter()}, TypedDictIterableUnion, use_async) == { + "FOO": [{"fooBaz": "hello"}, {"fooBaz": "world"}] + } + + +class TypedDictIterableUnionStr(TypedDict): + foo: Annotated[Union[str, Iterable[Baz8]], PropertyInfo(alias="FOO")] + + +@parametrize +@pytest.mark.asyncio +async def test_iterable_union_str(use_async: bool) -> None: + assert await transform({"foo": "bar"}, TypedDictIterableUnionStr, use_async) == {"FOO": "bar"} + assert cast(Any, await transform(iter([{"foo_baz": "bar"}]), Union[str, Iterable[Baz8]], use_async)) == [ + {"fooBaz": "bar"} + ] + + +class TypedDictBase64Input(TypedDict): + foo: Annotated[Union[str, Base64FileInput], PropertyInfo(format="base64")] + + +@parametrize +@pytest.mark.asyncio +async def test_base64_file_input(use_async: bool) -> None: + # strings are left as-is + assert await transform({"foo": "bar"}, TypedDictBase64Input, use_async) == {"foo": "bar"} + + # pathlib.Path is automatically converted to base64 + assert await transform({"foo": SAMPLE_FILE_PATH}, TypedDictBase64Input, use_async) == { + "foo": "SGVsbG8sIHdvcmxkIQo=" + } # type: ignore[comparison-overlap] + + # io instances are automatically converted to base64 + assert await transform({"foo": io.StringIO("Hello, world!")}, TypedDictBase64Input, use_async) == { + "foo": "SGVsbG8sIHdvcmxkIQ==" + } # type: ignore[comparison-overlap] + assert await transform({"foo": io.BytesIO(b"Hello, world!")}, TypedDictBase64Input, use_async) == { + "foo": "SGVsbG8sIHdvcmxkIQ==" + } # type: ignore[comparison-overlap] diff --git a/tests/test_utils/test_proxy.py b/tests/test_utils/test_proxy.py new file mode 100644 index 00000000..43cb3660 --- /dev/null +++ b/tests/test_utils/test_proxy.py @@ -0,0 +1,23 @@ +import operator +from typing import Any +from typing_extensions import override + +from writer_ai._utils import LazyProxy + + +class RecursiveLazyProxy(LazyProxy[Any]): + @override + def __load__(self) -> Any: + return self + + def __call__(self, *_args: Any, **_kwds: Any) -> Any: + raise RuntimeError("This should never be called!") + + +def test_recursive_proxy() -> None: + proxy = RecursiveLazyProxy() + assert repr(proxy) == "RecursiveLazyProxy" + assert str(proxy) == "RecursiveLazyProxy" + assert dir(proxy) == [] + assert type(proxy).__name__ == "RecursiveLazyProxy" + assert type(operator.attrgetter("name.foo.bar.baz")(proxy)).__name__ == "RecursiveLazyProxy" diff --git a/tests/test_utils/test_typing.py b/tests/test_utils/test_typing.py new file mode 100644 index 00000000..6bc943b1 --- /dev/null +++ b/tests/test_utils/test_typing.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +from typing import Generic, TypeVar, cast + +from writer_ai._utils import extract_type_var_from_base + +_T = TypeVar("_T") +_T2 = TypeVar("_T2") +_T3 = TypeVar("_T3") + + +class BaseGeneric(Generic[_T]): + ... + + +class SubclassGeneric(BaseGeneric[_T]): + ... + + +class BaseGenericMultipleTypeArgs(Generic[_T, _T2, _T3]): + ... + + +class SubclassGenericMultipleTypeArgs(BaseGenericMultipleTypeArgs[_T, _T2, _T3]): + ... + + +class SubclassDifferentOrderGenericMultipleTypeArgs(BaseGenericMultipleTypeArgs[_T2, _T, _T3]): + ... + + +def test_extract_type_var() -> None: + assert ( + extract_type_var_from_base( + BaseGeneric[int], + index=0, + generic_bases=cast("tuple[type, ...]", (BaseGeneric,)), + ) + == int + ) + + +def test_extract_type_var_generic_subclass() -> None: + assert ( + extract_type_var_from_base( + SubclassGeneric[int], + index=0, + generic_bases=cast("tuple[type, ...]", (BaseGeneric,)), + ) + == int + ) + + +def test_extract_type_var_multiple() -> None: + typ = BaseGenericMultipleTypeArgs[int, str, None] + + generic_bases = cast("tuple[type, ...]", (BaseGenericMultipleTypeArgs,)) + assert extract_type_var_from_base(typ, index=0, generic_bases=generic_bases) == int + assert extract_type_var_from_base(typ, index=1, generic_bases=generic_bases) == str + assert extract_type_var_from_base(typ, index=2, generic_bases=generic_bases) == type(None) + + +def test_extract_type_var_generic_subclass_multiple() -> None: + typ = SubclassGenericMultipleTypeArgs[int, str, None] + + generic_bases = cast("tuple[type, ...]", (BaseGenericMultipleTypeArgs,)) + assert extract_type_var_from_base(typ, index=0, generic_bases=generic_bases) == int + assert extract_type_var_from_base(typ, index=1, generic_bases=generic_bases) == str + assert extract_type_var_from_base(typ, index=2, generic_bases=generic_bases) == type(None) + + +def test_extract_type_var_generic_subclass_different_ordering_multiple() -> None: + typ = SubclassDifferentOrderGenericMultipleTypeArgs[int, str, None] + + generic_bases = cast("tuple[type, ...]", (BaseGenericMultipleTypeArgs,)) + assert extract_type_var_from_base(typ, index=0, generic_bases=generic_bases) == int + assert extract_type_var_from_base(typ, index=1, generic_bases=generic_bases) == str + assert extract_type_var_from_base(typ, index=2, generic_bases=generic_bases) == type(None) diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 00000000..73eff2bd --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,151 @@ +from __future__ import annotations + +import os +import inspect +import traceback +import contextlib +from typing import Any, TypeVar, Iterator, cast +from datetime import date, datetime +from typing_extensions import Literal, get_args, get_origin, assert_type + +from writer_ai._types import NoneType +from writer_ai._utils import ( + is_dict, + is_list, + is_list_type, + is_union_type, + extract_type_arg, + is_annotated_type, +) +from writer_ai._compat import PYDANTIC_V2, field_outer_type, get_model_fields +from writer_ai._models import BaseModel + +BaseModelT = TypeVar("BaseModelT", bound=BaseModel) + + +def assert_matches_model(model: type[BaseModelT], value: BaseModelT, *, path: list[str]) -> bool: + for name, field in get_model_fields(model).items(): + field_value = getattr(value, name) + if PYDANTIC_V2: + allow_none = False + else: + # in v1 nullability was structured differently + # https://docs.pydantic.dev/2.0/migration/#required-optional-and-nullable-fields + allow_none = getattr(field, "allow_none", False) + + assert_matches_type( + field_outer_type(field), + field_value, + path=[*path, name], + allow_none=allow_none, + ) + + return True + + +# Note: the `path` argument is only used to improve error messages when `--showlocals` is used +def assert_matches_type( + type_: Any, + value: object, + *, + path: list[str], + allow_none: bool = False, +) -> None: + # unwrap `Annotated[T, ...]` -> `T` + if is_annotated_type(type_): + type_ = extract_type_arg(type_, 0) + + if allow_none and value is None: + return + + if type_ is None or type_ is NoneType: + assert value is None + return + + origin = get_origin(type_) or type_ + + if is_list_type(type_): + return _assert_list_type(type_, value) + + if origin == str: + assert isinstance(value, str) + elif origin == int: + assert isinstance(value, int) + elif origin == bool: + assert isinstance(value, bool) + elif origin == float: + assert isinstance(value, float) + elif origin == bytes: + assert isinstance(value, bytes) + elif origin == datetime: + assert isinstance(value, datetime) + elif origin == date: + assert isinstance(value, date) + elif origin == object: + # nothing to do here, the expected type is unknown + pass + elif origin == Literal: + assert value in get_args(type_) + elif origin == dict: + assert is_dict(value) + + args = get_args(type_) + key_type = args[0] + items_type = args[1] + + for key, item in value.items(): + assert_matches_type(key_type, key, path=[*path, ""]) + assert_matches_type(items_type, item, path=[*path, ""]) + elif is_union_type(type_): + variants = get_args(type_) + + try: + none_index = variants.index(type(None)) + except ValueError: + pass + else: + # special case Optional[T] for better error messages + if len(variants) == 2: + if value is None: + # valid + return + + return assert_matches_type(type_=variants[not none_index], value=value, path=path) + + for i, variant in enumerate(variants): + try: + assert_matches_type(variant, value, path=[*path, f"variant {i}"]) + return + except AssertionError: + traceback.print_exc() + continue + + raise AssertionError("Did not match any variants") + elif issubclass(origin, BaseModel): + assert isinstance(value, type_) + assert assert_matches_model(type_, cast(Any, value), path=path) + elif inspect.isclass(origin) and origin.__name__ == "HttpxBinaryResponseContent": + assert value.__class__.__name__ == "HttpxBinaryResponseContent" + else: + assert None, f"Unhandled field type: {type_}" + + +def _assert_list_type(type_: type[object], value: object) -> None: + assert is_list(value) + + inner_type = get_args(type_)[0] + for entry in value: + assert_type(inner_type, entry) # type: ignore + + +@contextlib.contextmanager +def update_env(**new_env: str) -> Iterator[None]: + old = os.environ.copy() + + try: + os.environ.update(new_env) + + yield None + finally: + os.environ.clear() + os.environ.update(old) From 0a74007997de98da7a7e99cf5a70006acd21ffa7 Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Wed, 1 May 2024 19:48:27 +0000 Subject: [PATCH 002/399] feat(api): update via SDK Studio --- README.md | 6 +++--- src/writer_ai/_client.py | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index a1314426..4d58a991 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ from writer_ai import WriterAI client = WriterAI( # This is the default and can be omitted - api_key=os.environ.get("WRITERAI_AUTH_TOKEN"), + api_key=os.environ.get("WRITERAI_API_KEY"), ) chat_chat_response = client.chat.chat( @@ -49,7 +49,7 @@ print(chat_chat_response.id) While you can provide an `api_key` keyword argument, we recommend using [python-dotenv](https://pypi.org/project/python-dotenv/) -to add `WRITERAI_AUTH_TOKEN="My API Key"` to your `.env` file +to add `WRITERAI_API_KEY="My API Key"` to your `.env` file so that your API Key is not stored in source control. ## Async usage @@ -63,7 +63,7 @@ from writer_ai import AsyncWriterAI client = AsyncWriterAI( # This is the default and can be omitted - api_key=os.environ.get("WRITERAI_AUTH_TOKEN"), + api_key=os.environ.get("WRITERAI_API_KEY"), ) diff --git a/src/writer_ai/_client.py b/src/writer_ai/_client.py index 4bf2e328..4e9d8fee 100644 --- a/src/writer_ai/_client.py +++ b/src/writer_ai/_client.py @@ -80,13 +80,13 @@ def __init__( ) -> None: """Construct a new synchronous writerai client instance. - This automatically infers the `api_key` argument from the `WRITERAI_AUTH_TOKEN` environment variable if it is not provided. + This automatically infers the `api_key` argument from the `WRITERAI_API_KEY` environment variable if it is not provided. """ if api_key is None: - api_key = os.environ.get("WRITERAI_AUTH_TOKEN") + api_key = os.environ.get("WRITERAI_API_KEY") if api_key is None: raise WriterAIError( - "The api_key client option must be set either by passing api_key to the client or by setting the WRITERAI_AUTH_TOKEN environment variable" + "The api_key client option must be set either by passing api_key to the client or by setting the WRITERAI_API_KEY environment variable" ) self.api_key = api_key @@ -254,13 +254,13 @@ def __init__( ) -> None: """Construct a new async writerai client instance. - This automatically infers the `api_key` argument from the `WRITERAI_AUTH_TOKEN` environment variable if it is not provided. + This automatically infers the `api_key` argument from the `WRITERAI_API_KEY` environment variable if it is not provided. """ if api_key is None: - api_key = os.environ.get("WRITERAI_AUTH_TOKEN") + api_key = os.environ.get("WRITERAI_API_KEY") if api_key is None: raise WriterAIError( - "The api_key client option must be set either by passing api_key to the client or by setting the WRITERAI_AUTH_TOKEN environment variable" + "The api_key client option must be set either by passing api_key to the client or by setting the WRITERAI_API_KEY environment variable" ) self.api_key = api_key From d39dfe81c3cc5a0588acbecacb89de284b6ffaf4 Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Wed, 1 May 2024 20:17:56 +0000 Subject: [PATCH 003/399] feat(api): update via SDK Studio --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index eb651b7b..6a845085 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ description = "The official Python library for the writerai API" dynamic = ["readme"] license = "Apache-2.0" authors = [ -{ name = "Writer AI", email = "dev-feedback@writer.com" }, +{ name = "Writer AI", email = "dev-feedbacik@writer.com" }, ] dependencies = [ "httpx>=0.23.0, <1", From c79dd69ba93d3280a99c4f4691fc5a42c2fd3c44 Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Wed, 1 May 2024 20:18:09 +0000 Subject: [PATCH 004/399] feat(api): update via SDK Studio --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6a845085..eb651b7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ description = "The official Python library for the writerai API" dynamic = ["readme"] license = "Apache-2.0" authors = [ -{ name = "Writer AI", email = "dev-feedbacik@writer.com" }, +{ name = "Writer AI", email = "dev-feedback@writer.com" }, ] dependencies = [ "httpx>=0.23.0, <1", From d08a52a59383d4d00dc6b5d177acea535b11eb20 Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Wed, 1 May 2024 20:22:15 +0000 Subject: [PATCH 005/399] feat(api): update via SDK Studio --- .github/workflows/ci.yml | 2 +- CONTRIBUTING.md | 2 +- README.md | 42 +++++++++---------- api.md | 12 +++--- mypy.ini | 2 +- pyproject.toml | 6 +-- src/{writer_ai => writerai}/__init__.py | 4 +- src/{writer_ai => writerai}/_base_client.py | 0 src/{writer_ai => writerai}/_client.py | 4 +- src/{writer_ai => writerai}/_compat.py | 0 src/{writer_ai => writerai}/_constants.py | 0 src/{writer_ai => writerai}/_exceptions.py | 0 src/{writer_ai => writerai}/_files.py | 0 src/{writer_ai => writerai}/_models.py | 0 src/{writer_ai => writerai}/_qs.py | 0 src/{writer_ai => writerai}/_resource.py | 0 src/{writer_ai => writerai}/_response.py | 8 ++-- src/{writer_ai => writerai}/_streaming.py | 0 src/{writer_ai => writerai}/_types.py | 2 +- .../_utils/__init__.py | 0 src/{writer_ai => writerai}/_utils/_logs.py | 6 +-- src/{writer_ai => writerai}/_utils/_proxy.py | 0 .../_utils/_streams.py | 0 src/{writer_ai => writerai}/_utils/_sync.py | 0 .../_utils/_transform.py | 0 src/{writer_ai => writerai}/_utils/_typing.py | 0 src/{writer_ai => writerai}/_utils/_utils.py | 0 src/{writer_ai => writerai}/_version.py | 2 +- src/writerai/lib/.keep | 4 ++ src/{writer_ai => writerai}/py.typed | 0 .../resources/__init__.py | 0 src/{writer_ai => writerai}/resources/chat.py | 0 .../resources/completions.py | 0 .../resources/models.py | 0 src/{writer_ai => writerai}/types/__init__.py | 0 .../types/chat_chat_params.py | 0 .../types/chat_chat_response.py | 0 .../types/completion.py | 0 .../types/completion_create_params.py | 0 .../types/model_list_response.py | 0 .../types/streaming_data.py | 0 tests/api_resources/test_chat.py | 4 +- tests/api_resources/test_completions.py | 4 +- tests/api_resources/test_models.py | 4 +- tests/conftest.py | 4 +- tests/test_client.py | 36 ++++++++-------- tests/test_deepcopy.py | 2 +- tests/test_extract_files.py | 4 +- tests/test_files.py | 2 +- tests/test_models.py | 6 +-- tests/test_qs.py | 2 +- tests/test_required_args.py | 2 +- tests/test_response.py | 14 +++---- tests/test_streaming.py | 4 +- tests/test_transform.py | 8 ++-- tests/test_utils/test_proxy.py | 2 +- tests/test_utils/test_typing.py | 2 +- tests/utils.py | 8 ++-- 58 files changed, 104 insertions(+), 100 deletions(-) rename src/{writer_ai => writerai}/__init__.py (95%) rename src/{writer_ai => writerai}/_base_client.py (100%) rename src/{writer_ai => writerai}/_client.py (99%) rename src/{writer_ai => writerai}/_compat.py (100%) rename src/{writer_ai => writerai}/_constants.py (100%) rename src/{writer_ai => writerai}/_exceptions.py (100%) rename src/{writer_ai => writerai}/_files.py (100%) rename src/{writer_ai => writerai}/_models.py (100%) rename src/{writer_ai => writerai}/_qs.py (100%) rename src/{writer_ai => writerai}/_resource.py (100%) rename src/{writer_ai => writerai}/_response.py (99%) rename src/{writer_ai => writerai}/_streaming.py (100%) rename src/{writer_ai => writerai}/_types.py (99%) rename src/{writer_ai => writerai}/_utils/__init__.py (100%) rename src/{writer_ai => writerai}/_utils/_logs.py (71%) rename src/{writer_ai => writerai}/_utils/_proxy.py (100%) rename src/{writer_ai => writerai}/_utils/_streams.py (100%) rename src/{writer_ai => writerai}/_utils/_sync.py (100%) rename src/{writer_ai => writerai}/_utils/_transform.py (100%) rename src/{writer_ai => writerai}/_utils/_typing.py (100%) rename src/{writer_ai => writerai}/_utils/_utils.py (100%) rename src/{writer_ai => writerai}/_version.py (82%) create mode 100644 src/writerai/lib/.keep rename src/{writer_ai => writerai}/py.typed (100%) rename src/{writer_ai => writerai}/resources/__init__.py (100%) rename src/{writer_ai => writerai}/resources/chat.py (100%) rename src/{writer_ai => writerai}/resources/completions.py (100%) rename src/{writer_ai => writerai}/resources/models.py (100%) rename src/{writer_ai => writerai}/types/__init__.py (100%) rename src/{writer_ai => writerai}/types/chat_chat_params.py (100%) rename src/{writer_ai => writerai}/types/chat_chat_response.py (100%) rename src/{writer_ai => writerai}/types/completion.py (100%) rename src/{writer_ai => writerai}/types/completion_create_params.py (100%) rename src/{writer_ai => writerai}/types/model_list_response.py (100%) rename src/{writer_ai => writerai}/types/streaming_data.py (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c9b189a7..b2a2592a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,7 @@ jobs: - name: Ensure importable run: | - rye run python -c 'import writer_ai' + rye run python -c 'import writerai' test: name: test runs-on: ubuntu-latest diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7a6dbff0..26c16c10 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,7 +32,7 @@ $ pip install -r requirements-dev.lock ## Modifying/Adding code Most of the SDK is generated code, and any modified code will be overridden on the next generation. The -`src/writer_ai/lib/` and `examples/` directories are exceptions and will never be overridden. +`src/writerai/lib/` and `examples/` directories are exceptions and will never be overridden. ## Adding and running examples diff --git a/README.md b/README.md index 4d58a991..23aab7c5 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ The full API of this library can be found in [api.md](api.md). ```python import os -from writer_ai import WriterAI +from writerai import WriterAI client = WriterAI( # This is the default and can be omitted @@ -59,7 +59,7 @@ Simply import `AsyncWriterAI` instead of `WriterAI` and use `await` with each AP ```python import os import asyncio -from writer_ai import AsyncWriterAI +from writerai import AsyncWriterAI client = AsyncWriterAI( # This is the default and can be omitted @@ -90,7 +90,7 @@ Functionality between the synchronous and asynchronous clients is otherwise iden We provide support for streaming responses using Server Side Events (SSE). ```python -from writer_ai import WriterAI +from writerai import WriterAI client = WriterAI() @@ -106,7 +106,7 @@ for completion in stream: The async client uses the exact same interface. ```python -from writer_ai import AsyncWriterAI +from writerai import AsyncWriterAI client = AsyncWriterAI() @@ -130,16 +130,16 @@ Typed requests and responses provide autocomplete and documentation within your ## Handling errors -When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `writer_ai.APIConnectionError` is raised. +When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `writerai.APIConnectionError` is raised. When the API returns a non-success status code (that is, 4xx or 5xx -response), a subclass of `writer_ai.APIStatusError` is raised, containing `status_code` and `response` properties. +response), a subclass of `writerai.APIStatusError` is raised, containing `status_code` and `response` properties. -All errors inherit from `writer_ai.APIError`. +All errors inherit from `writerai.APIError`. ```python -import writer_ai -from writer_ai import WriterAI +import writerai +from writerai import WriterAI client = WriterAI() @@ -153,12 +153,12 @@ try: ], model="string", ) -except writer_ai.APIConnectionError as e: +except writerai.APIConnectionError as e: print("The server could not be reached") print(e.__cause__) # an underlying Exception, likely raised within httpx. -except writer_ai.RateLimitError as e: +except writerai.RateLimitError as e: print("A 429 status code was received; we should back off a bit.") -except writer_ai.APIStatusError as e: +except writerai.APIStatusError as e: print("Another non-200-range status code was received") print(e.status_code) print(e.response) @@ -186,7 +186,7 @@ Connection errors (for example, due to a network connectivity problem), 408 Requ You can use the `max_retries` option to configure or disable retry settings: ```python -from writer_ai import WriterAI +from writerai import WriterAI # Configure the default for all requests: client = WriterAI( @@ -212,7 +212,7 @@ By default requests time out after 1 minute. You can configure this with a `time which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/#fine-tuning-the-configuration) object: ```python -from writer_ai import WriterAI +from writerai import WriterAI # Configure the default for all requests: client = WriterAI( @@ -247,10 +247,10 @@ Note that requests that time out are [retried twice by default](#retries). We use the standard library [`logging`](https://docs.python.org/3/library/logging.html) module. -You can enable logging by setting the environment variable `WRITER_AI_LOG` to `debug`. +You can enable logging by setting the environment variable `WRITERAI_LOG` to `debug`. ```shell -$ export WRITER_AI_LOG=debug +$ export WRITERAI_LOG=debug ``` ### How to tell whether `None` means `null` or missing @@ -270,7 +270,7 @@ if response.my_field is None: The "raw" Response object can be accessed by prefixing `.with_raw_response.` to any HTTP method call, e.g., ```py -from writer_ai import WriterAI +from writerai import WriterAI client = WriterAI() response = client.chat.with_raw_response.chat( @@ -286,9 +286,9 @@ chat = response.parse() # get the object that `chat.chat()` would have returned print(chat.id) ``` -These methods return an [`APIResponse`](https://github.com/stainless-sdks/tree/main/src/writer_ai/_response.py) object. +These methods return an [`APIResponse`](https://github.com/stainless-sdks/tree/main/src/writerai/_response.py) object. -The async client returns an [`AsyncAPIResponse`](https://github.com/stainless-sdks/tree/main/src/writer_ai/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. +The async client returns an [`AsyncAPIResponse`](https://github.com/stainless-sdks/tree/main/src/writerai/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. #### `.with_streaming_response` @@ -358,10 +358,10 @@ You can directly override the [httpx client](https://www.python-httpx.org/api/#c - Additional [advanced](https://www.python-httpx.org/advanced/#client-instances) functionality ```python -from writer_ai import WriterAI, DefaultHttpxClient +from writerai import WriterAI, DefaultHttpxClient client = WriterAI( - # Or use the `WRITER_AI_BASE_URL` env var + # Or use the `WRITERAI_BASE_URL` env var base_url="http://my.test.server.example.com:8083", http_client=DefaultHttpxClient( proxies="http://my.test.proxy.example.com", diff --git a/api.md b/api.md index d5378453..71e75e55 100644 --- a/api.md +++ b/api.md @@ -3,33 +3,33 @@ Types: ```python -from writer_ai.types import ChatChatResponse +from writerai.types import ChatChatResponse ``` Methods: -- client.chat.chat(\*\*params) -> ChatChatResponse +- client.chat.chat(\*\*params) -> ChatChatResponse # Completions Types: ```python -from writer_ai.types import Completion, StreamingData +from writerai.types import Completion, StreamingData ``` Methods: -- client.completions.create(\*\*params) -> Completion +- client.completions.create(\*\*params) -> Completion # Models Types: ```python -from writer_ai.types import ModelListResponse +from writerai.types import ModelListResponse ``` Methods: -- client.models.list() -> ModelListResponse +- client.models.list() -> ModelListResponse diff --git a/mypy.ini b/mypy.ini index 5da83123..1ece7ab6 100644 --- a/mypy.ini +++ b/mypy.ini @@ -5,7 +5,7 @@ show_error_codes = True # Exclude _files.py because mypy isn't smart enough to apply # the correct type narrowing and as this is an internal module # it's fine to just use Pyright. -exclude = ^(src/writer_ai/_files\.py|_dev/.*\.py)$ +exclude = ^(src/writerai/_files\.py|_dev/.*\.py)$ strict_equality = True implicit_reexport = True diff --git a/pyproject.toml b/pyproject.toml index eb651b7b..795b0519 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,7 +84,7 @@ typecheck = { chain = [ "typecheck:mypy" ]} "typecheck:pyright" = "pyright" -"typecheck:verify-types" = "pyright --verifytypes writer_ai --ignoreexternal" +"typecheck:verify-types" = "pyright --verifytypes writerai --ignoreexternal" "typecheck:mypy" = "mypy ." [build-system] @@ -97,7 +97,7 @@ include = [ ] [tool.hatch.build.targets.wheel] -packages = ["src/writer_ai"] +packages = ["src/writerai"] [tool.hatch.metadata.hooks.fancy-pypi-readme] content-type = "text/markdown" @@ -187,7 +187,7 @@ length-sort = true length-sort-straight = true combine-as-imports = true extra-standard-library = ["typing_extensions"] -known-first-party = ["writer_ai", "tests"] +known-first-party = ["writerai", "tests"] [tool.ruff.per-file-ignores] "bin/**.py" = ["T201", "T203"] diff --git a/src/writer_ai/__init__.py b/src/writerai/__init__.py similarity index 95% rename from src/writer_ai/__init__.py rename to src/writerai/__init__.py index 8c76f13a..2c51f189 100644 --- a/src/writer_ai/__init__.py +++ b/src/writerai/__init__.py @@ -82,12 +82,12 @@ # Update the __module__ attribute for exported symbols so that # error messages point to this module instead of the module # it was originally defined in, e.g. -# writer_ai._exceptions.NotFoundError -> writer_ai.NotFoundError +# writerai._exceptions.NotFoundError -> writerai.NotFoundError __locals = locals() for __name in __all__: if not __name.startswith("__"): try: - __locals[__name].__module__ = "writer_ai" + __locals[__name].__module__ = "writerai" except (TypeError, AttributeError): # Some of our exported symbols are builtins which we can't set attributes for. pass diff --git a/src/writer_ai/_base_client.py b/src/writerai/_base_client.py similarity index 100% rename from src/writer_ai/_base_client.py rename to src/writerai/_base_client.py diff --git a/src/writer_ai/_client.py b/src/writerai/_client.py similarity index 99% rename from src/writer_ai/_client.py rename to src/writerai/_client.py index 4e9d8fee..2492ff3a 100644 --- a/src/writer_ai/_client.py +++ b/src/writerai/_client.py @@ -91,7 +91,7 @@ def __init__( self.api_key = api_key if base_url is None: - base_url = os.environ.get("WRITER_AI_BASE_URL") + base_url = os.environ.get("WRITERAI_BASE_URL") if base_url is None: base_url = f"https://api.qordobadev.com" @@ -265,7 +265,7 @@ def __init__( self.api_key = api_key if base_url is None: - base_url = os.environ.get("WRITER_AI_BASE_URL") + base_url = os.environ.get("WRITERAI_BASE_URL") if base_url is None: base_url = f"https://api.qordobadev.com" diff --git a/src/writer_ai/_compat.py b/src/writerai/_compat.py similarity index 100% rename from src/writer_ai/_compat.py rename to src/writerai/_compat.py diff --git a/src/writer_ai/_constants.py b/src/writerai/_constants.py similarity index 100% rename from src/writer_ai/_constants.py rename to src/writerai/_constants.py diff --git a/src/writer_ai/_exceptions.py b/src/writerai/_exceptions.py similarity index 100% rename from src/writer_ai/_exceptions.py rename to src/writerai/_exceptions.py diff --git a/src/writer_ai/_files.py b/src/writerai/_files.py similarity index 100% rename from src/writer_ai/_files.py rename to src/writerai/_files.py diff --git a/src/writer_ai/_models.py b/src/writerai/_models.py similarity index 100% rename from src/writer_ai/_models.py rename to src/writerai/_models.py diff --git a/src/writer_ai/_qs.py b/src/writerai/_qs.py similarity index 100% rename from src/writer_ai/_qs.py rename to src/writerai/_qs.py diff --git a/src/writer_ai/_resource.py b/src/writerai/_resource.py similarity index 100% rename from src/writer_ai/_resource.py rename to src/writerai/_resource.py diff --git a/src/writer_ai/_response.py b/src/writerai/_response.py similarity index 99% rename from src/writer_ai/_response.py rename to src/writerai/_response.py index 98e579f9..7739bb85 100644 --- a/src/writer_ai/_response.py +++ b/src/writerai/_response.py @@ -203,7 +203,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: return cast(R, response) if inspect.isclass(origin) and not issubclass(origin, BaseModel) and issubclass(origin, pydantic.BaseModel): - raise TypeError("Pydantic models must subclass our base model type, e.g. `from writer_ai import BaseModel`") + raise TypeError("Pydantic models must subclass our base model type, e.g. `from writerai import BaseModel`") if ( cast_to is not object @@ -271,7 +271,7 @@ def parse(self, *, to: type[_T] | None = None) -> R | _T: the `to` argument, e.g. ```py - from writer_ai import BaseModel + from writerai import BaseModel class MyModel(BaseModel): @@ -375,7 +375,7 @@ async def parse(self, *, to: type[_T] | None = None) -> R | _T: the `to` argument, e.g. ```py - from writer_ai import BaseModel + from writerai import BaseModel class MyModel(BaseModel): @@ -546,7 +546,7 @@ async def stream_to_file( class MissingStreamClassError(TypeError): def __init__(self) -> None: super().__init__( - "The `stream` argument was set to `True` but the `stream_cls` argument was not given. See `writer_ai._streaming` for reference", + "The `stream` argument was set to `True` but the `stream_cls` argument was not given. See `writerai._streaming` for reference", ) diff --git a/src/writer_ai/_streaming.py b/src/writerai/_streaming.py similarity index 100% rename from src/writer_ai/_streaming.py rename to src/writerai/_streaming.py diff --git a/src/writer_ai/_types.py b/src/writerai/_types.py similarity index 99% rename from src/writer_ai/_types.py rename to src/writerai/_types.py index 9281309d..7dbbaa54 100644 --- a/src/writer_ai/_types.py +++ b/src/writerai/_types.py @@ -81,7 +81,7 @@ # This unfortunately means that you will either have # to import this type and pass it explicitly: # -# from writer_ai import NoneType +# from writerai import NoneType # client.get('/foo', cast_to=NoneType) # # or build it yourself: diff --git a/src/writer_ai/_utils/__init__.py b/src/writerai/_utils/__init__.py similarity index 100% rename from src/writer_ai/_utils/__init__.py rename to src/writerai/_utils/__init__.py diff --git a/src/writer_ai/_utils/_logs.py b/src/writerai/_utils/_logs.py similarity index 71% rename from src/writer_ai/_utils/_logs.py rename to src/writerai/_utils/_logs.py index 694fbaa4..7dad39a6 100644 --- a/src/writer_ai/_utils/_logs.py +++ b/src/writerai/_utils/_logs.py @@ -1,12 +1,12 @@ import os import logging -logger: logging.Logger = logging.getLogger("writer_ai") +logger: logging.Logger = logging.getLogger("writerai") httpx_logger: logging.Logger = logging.getLogger("httpx") def _basic_config() -> None: - # e.g. [2023-10-05 14:12:26 - writer_ai._base_client:818 - DEBUG] HTTP Request: POST http://127.0.0.1:4010/foo/bar "200 OK" + # e.g. [2023-10-05 14:12:26 - writerai._base_client:818 - DEBUG] HTTP Request: POST http://127.0.0.1:4010/foo/bar "200 OK" logging.basicConfig( format="[%(asctime)s - %(name)s:%(lineno)d - %(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S", @@ -14,7 +14,7 @@ def _basic_config() -> None: def setup_logging() -> None: - env = os.environ.get("WRITER_AI_LOG") + env = os.environ.get("WRITERAI_LOG") if env == "debug": _basic_config() logger.setLevel(logging.DEBUG) diff --git a/src/writer_ai/_utils/_proxy.py b/src/writerai/_utils/_proxy.py similarity index 100% rename from src/writer_ai/_utils/_proxy.py rename to src/writerai/_utils/_proxy.py diff --git a/src/writer_ai/_utils/_streams.py b/src/writerai/_utils/_streams.py similarity index 100% rename from src/writer_ai/_utils/_streams.py rename to src/writerai/_utils/_streams.py diff --git a/src/writer_ai/_utils/_sync.py b/src/writerai/_utils/_sync.py similarity index 100% rename from src/writer_ai/_utils/_sync.py rename to src/writerai/_utils/_sync.py diff --git a/src/writer_ai/_utils/_transform.py b/src/writerai/_utils/_transform.py similarity index 100% rename from src/writer_ai/_utils/_transform.py rename to src/writerai/_utils/_transform.py diff --git a/src/writer_ai/_utils/_typing.py b/src/writerai/_utils/_typing.py similarity index 100% rename from src/writer_ai/_utils/_typing.py rename to src/writerai/_utils/_typing.py diff --git a/src/writer_ai/_utils/_utils.py b/src/writerai/_utils/_utils.py similarity index 100% rename from src/writer_ai/_utils/_utils.py rename to src/writerai/_utils/_utils.py diff --git a/src/writer_ai/_version.py b/src/writerai/_version.py similarity index 82% rename from src/writer_ai/_version.py rename to src/writerai/_version.py index 47b03f45..4fca0ec6 100644 --- a/src/writer_ai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -__title__ = "writer_ai" +__title__ = "writerai" __version__ = "0.0.1-alpha.0" diff --git a/src/writerai/lib/.keep b/src/writerai/lib/.keep new file mode 100644 index 00000000..5e2c99fd --- /dev/null +++ b/src/writerai/lib/.keep @@ -0,0 +1,4 @@ +File generated from our OpenAPI spec by Stainless. + +This directory can be used to store custom files to expand the SDK. +It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. \ No newline at end of file diff --git a/src/writer_ai/py.typed b/src/writerai/py.typed similarity index 100% rename from src/writer_ai/py.typed rename to src/writerai/py.typed diff --git a/src/writer_ai/resources/__init__.py b/src/writerai/resources/__init__.py similarity index 100% rename from src/writer_ai/resources/__init__.py rename to src/writerai/resources/__init__.py diff --git a/src/writer_ai/resources/chat.py b/src/writerai/resources/chat.py similarity index 100% rename from src/writer_ai/resources/chat.py rename to src/writerai/resources/chat.py diff --git a/src/writer_ai/resources/completions.py b/src/writerai/resources/completions.py similarity index 100% rename from src/writer_ai/resources/completions.py rename to src/writerai/resources/completions.py diff --git a/src/writer_ai/resources/models.py b/src/writerai/resources/models.py similarity index 100% rename from src/writer_ai/resources/models.py rename to src/writerai/resources/models.py diff --git a/src/writer_ai/types/__init__.py b/src/writerai/types/__init__.py similarity index 100% rename from src/writer_ai/types/__init__.py rename to src/writerai/types/__init__.py diff --git a/src/writer_ai/types/chat_chat_params.py b/src/writerai/types/chat_chat_params.py similarity index 100% rename from src/writer_ai/types/chat_chat_params.py rename to src/writerai/types/chat_chat_params.py diff --git a/src/writer_ai/types/chat_chat_response.py b/src/writerai/types/chat_chat_response.py similarity index 100% rename from src/writer_ai/types/chat_chat_response.py rename to src/writerai/types/chat_chat_response.py diff --git a/src/writer_ai/types/completion.py b/src/writerai/types/completion.py similarity index 100% rename from src/writer_ai/types/completion.py rename to src/writerai/types/completion.py diff --git a/src/writer_ai/types/completion_create_params.py b/src/writerai/types/completion_create_params.py similarity index 100% rename from src/writer_ai/types/completion_create_params.py rename to src/writerai/types/completion_create_params.py diff --git a/src/writer_ai/types/model_list_response.py b/src/writerai/types/model_list_response.py similarity index 100% rename from src/writer_ai/types/model_list_response.py rename to src/writerai/types/model_list_response.py diff --git a/src/writer_ai/types/streaming_data.py b/src/writerai/types/streaming_data.py similarity index 100% rename from src/writer_ai/types/streaming_data.py rename to src/writerai/types/streaming_data.py diff --git a/tests/api_resources/test_chat.py b/tests/api_resources/test_chat.py index a1ae3271..c1c0f313 100644 --- a/tests/api_resources/test_chat.py +++ b/tests/api_resources/test_chat.py @@ -7,9 +7,9 @@ import pytest -from writer_ai import WriterAI, AsyncWriterAI +from writerai import WriterAI, AsyncWriterAI from tests.utils import assert_matches_type -from writer_ai.types import ChatChatResponse +from writerai.types import ChatChatResponse base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") diff --git a/tests/api_resources/test_completions.py b/tests/api_resources/test_completions.py index 11781207..70172d6f 100644 --- a/tests/api_resources/test_completions.py +++ b/tests/api_resources/test_completions.py @@ -7,9 +7,9 @@ import pytest -from writer_ai import WriterAI, AsyncWriterAI +from writerai import WriterAI, AsyncWriterAI from tests.utils import assert_matches_type -from writer_ai.types import Completion +from writerai.types import Completion base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") diff --git a/tests/api_resources/test_models.py b/tests/api_resources/test_models.py index 8c032fd8..083fd6ce 100644 --- a/tests/api_resources/test_models.py +++ b/tests/api_resources/test_models.py @@ -7,9 +7,9 @@ import pytest -from writer_ai import WriterAI, AsyncWriterAI +from writerai import WriterAI, AsyncWriterAI from tests.utils import assert_matches_type -from writer_ai.types import ModelListResponse +from writerai.types import ModelListResponse base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") diff --git a/tests/conftest.py b/tests/conftest.py index 1b0b32e6..91b8fbf0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,14 +7,14 @@ import pytest -from writer_ai import WriterAI, AsyncWriterAI +from writerai import WriterAI, AsyncWriterAI if TYPE_CHECKING: from _pytest.fixtures import FixtureRequest pytest.register_assert_rewrite("tests.utils") -logging.getLogger("writer_ai").setLevel(logging.DEBUG) +logging.getLogger("writerai").setLevel(logging.DEBUG) @pytest.fixture(scope="session") diff --git a/tests/test_client.py b/tests/test_client.py index 6cfd6c75..aaa549f3 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -16,12 +16,12 @@ from respx import MockRouter from pydantic import ValidationError -from writer_ai import WriterAI, AsyncWriterAI, APIResponseValidationError -from writer_ai._models import BaseModel, FinalRequestOptions -from writer_ai._constants import RAW_RESPONSE_HEADER -from writer_ai._streaming import Stream, AsyncStream -from writer_ai._exceptions import WriterAIError, APIStatusError, APITimeoutError, APIResponseValidationError -from writer_ai._base_client import ( +from writerai import WriterAI, AsyncWriterAI, APIResponseValidationError +from writerai._models import BaseModel, FinalRequestOptions +from writerai._constants import RAW_RESPONSE_HEADER +from writerai._streaming import Stream, AsyncStream +from writerai._exceptions import WriterAIError, APIStatusError, APITimeoutError, APIResponseValidationError +from writerai._base_client import ( DEFAULT_TIMEOUT, HTTPX_DEFAULT_TIMEOUT, BaseClient, @@ -225,10 +225,10 @@ def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.Statistic # to_raw_response_wrapper leaks through the @functools.wraps() decorator. # # removing the decorator fixes the leak for reasons we don't understand. - "writer_ai/_legacy_response.py", - "writer_ai/_response.py", + "writerai/_legacy_response.py", + "writerai/_response.py", # pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason. - "writer_ai/_compat.py", + "writerai/_compat.py", # Standard library leaks we don't care about. "/logging/__init__.py", ] @@ -548,7 +548,7 @@ def test_base_url_setter(self) -> None: assert client.base_url == "https://example.com/from_setter/" def test_base_url_env(self) -> None: - with update_env(WRITER_AI_BASE_URL="http://localhost:5000/from/env"): + with update_env(WRITERAI_BASE_URL="http://localhost:5000/from/env"): client = WriterAI(api_key=api_key, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" @@ -713,7 +713,7 @@ def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str calculated = client._calculate_retry_timeout(remaining_retries, options, headers) assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType] - @mock.patch("writer_ai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @mock.patch("writerai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: respx_mock.post("/v1/chat").mock(side_effect=httpx.TimeoutException("Test timeout error")) @@ -739,7 +739,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> No assert _get_open_connections(self.client) == 0 - @mock.patch("writer_ai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @mock.patch("writerai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: respx_mock.post("/v1/chat").mock(return_value=httpx.Response(500)) @@ -941,10 +941,10 @@ def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.Statistic # to_raw_response_wrapper leaks through the @functools.wraps() decorator. # # removing the decorator fixes the leak for reasons we don't understand. - "writer_ai/_legacy_response.py", - "writer_ai/_response.py", + "writerai/_legacy_response.py", + "writerai/_response.py", # pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason. - "writer_ai/_compat.py", + "writerai/_compat.py", # Standard library leaks we don't care about. "/logging/__init__.py", ] @@ -1266,7 +1266,7 @@ def test_base_url_setter(self) -> None: assert client.base_url == "https://example.com/from_setter/" def test_base_url_env(self) -> None: - with update_env(WRITER_AI_BASE_URL="http://localhost:5000/from/env"): + with update_env(WRITERAI_BASE_URL="http://localhost:5000/from/env"): client = AsyncWriterAI(api_key=api_key, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" @@ -1444,7 +1444,7 @@ async def test_parse_retry_after_header(self, remaining_retries: int, retry_afte calculated = client._calculate_retry_timeout(remaining_retries, options, headers) assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType] - @mock.patch("writer_ai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @mock.patch("writerai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: respx_mock.post("/v1/chat").mock(side_effect=httpx.TimeoutException("Test timeout error")) @@ -1470,7 +1470,7 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) assert _get_open_connections(self.client) == 0 - @mock.patch("writer_ai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @mock.patch("writerai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: respx_mock.post("/v1/chat").mock(return_value=httpx.Response(500)) diff --git a/tests/test_deepcopy.py b/tests/test_deepcopy.py index c6608761..628e4049 100644 --- a/tests/test_deepcopy.py +++ b/tests/test_deepcopy.py @@ -1,4 +1,4 @@ -from writer_ai._utils import deepcopy_minimal +from writerai._utils import deepcopy_minimal def assert_different_identities(obj1: object, obj2: object) -> None: diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py index 55d0fcd3..9d9a4b19 100644 --- a/tests/test_extract_files.py +++ b/tests/test_extract_files.py @@ -4,8 +4,8 @@ import pytest -from writer_ai._types import FileTypes -from writer_ai._utils import extract_files +from writerai._types import FileTypes +from writerai._utils import extract_files def test_removes_files_from_input() -> None: diff --git a/tests/test_files.py b/tests/test_files.py index cc8247bf..9cc66fe2 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -4,7 +4,7 @@ import pytest from dirty_equals import IsDict, IsList, IsBytes, IsTuple -from writer_ai._files import to_httpx_files, async_to_httpx_files +from writerai._files import to_httpx_files, async_to_httpx_files readme_path = Path(__file__).parent.parent.joinpath("README.md") diff --git a/tests/test_models.py b/tests/test_models.py index e0f8cee4..141feed2 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -7,9 +7,9 @@ import pydantic from pydantic import Field -from writer_ai._utils import PropertyInfo -from writer_ai._compat import PYDANTIC_V2, parse_obj, model_dump, model_json -from writer_ai._models import BaseModel, construct_type +from writerai._utils import PropertyInfo +from writerai._compat import PYDANTIC_V2, parse_obj, model_dump, model_json +from writerai._models import BaseModel, construct_type class BasicModel(BaseModel): diff --git a/tests/test_qs.py b/tests/test_qs.py index e1d4f943..3dbda131 100644 --- a/tests/test_qs.py +++ b/tests/test_qs.py @@ -4,7 +4,7 @@ import pytest -from writer_ai._qs import Querystring, stringify +from writerai._qs import Querystring, stringify def test_empty() -> None: diff --git a/tests/test_required_args.py b/tests/test_required_args.py index f3cf767a..db5f699c 100644 --- a/tests/test_required_args.py +++ b/tests/test_required_args.py @@ -2,7 +2,7 @@ import pytest -from writer_ai._utils import required_args +from writerai._utils import required_args def test_too_many_positional_params() -> None: diff --git a/tests/test_response.py b/tests/test_response.py index d26a4c93..769d7f7f 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -6,8 +6,8 @@ import pytest import pydantic -from writer_ai import WriterAI, BaseModel, AsyncWriterAI -from writer_ai._response import ( +from writerai import WriterAI, BaseModel, AsyncWriterAI +from writerai._response import ( APIResponse, BaseAPIResponse, AsyncAPIResponse, @@ -15,8 +15,8 @@ AsyncBinaryAPIResponse, extract_response_type, ) -from writer_ai._streaming import Stream -from writer_ai._base_client import FinalRequestOptions +from writerai._streaming import Stream +from writerai._base_client import FinalRequestOptions class ConcreteBaseAPIResponse(APIResponse[bytes]): @@ -40,7 +40,7 @@ def test_extract_response_type_direct_classes() -> None: def test_extract_response_type_direct_class_missing_type_arg() -> None: with pytest.raises( RuntimeError, - match="Expected type to have a type argument at index 0 but it did not", + match="Expected type to have a type argument at index 0 but it did not", ): extract_response_type(AsyncAPIResponse) @@ -72,7 +72,7 @@ def test_response_parse_mismatched_basemodel(client: WriterAI) -> None: with pytest.raises( TypeError, - match="Pydantic models must subclass our base model type, e.g. `from writer_ai import BaseModel`", + match="Pydantic models must subclass our base model type, e.g. `from writerai import BaseModel`", ): response.parse(to=PydanticModel) @@ -90,7 +90,7 @@ async def test_async_response_parse_mismatched_basemodel(async_client: AsyncWrit with pytest.raises( TypeError, - match="Pydantic models must subclass our base model type, e.g. `from writer_ai import BaseModel`", + match="Pydantic models must subclass our base model type, e.g. `from writerai import BaseModel`", ): await response.parse(to=PydanticModel) diff --git a/tests/test_streaming.py b/tests/test_streaming.py index 05855a23..13dda1e2 100644 --- a/tests/test_streaming.py +++ b/tests/test_streaming.py @@ -5,8 +5,8 @@ import httpx import pytest -from writer_ai import WriterAI, AsyncWriterAI -from writer_ai._streaming import Stream, AsyncStream, ServerSentEvent +from writerai import WriterAI, AsyncWriterAI +from writerai._streaming import Stream, AsyncStream, ServerSentEvent @pytest.mark.asyncio diff --git a/tests/test_transform.py b/tests/test_transform.py index 0ff64a7e..e0265127 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -8,15 +8,15 @@ import pytest -from writer_ai._types import Base64FileInput -from writer_ai._utils import ( +from writerai._types import Base64FileInput +from writerai._utils import ( PropertyInfo, transform as _transform, parse_datetime, async_transform as _async_transform, ) -from writer_ai._compat import PYDANTIC_V2 -from writer_ai._models import BaseModel +from writerai._compat import PYDANTIC_V2 +from writerai._models import BaseModel _T = TypeVar("_T") diff --git a/tests/test_utils/test_proxy.py b/tests/test_utils/test_proxy.py index 43cb3660..449f727d 100644 --- a/tests/test_utils/test_proxy.py +++ b/tests/test_utils/test_proxy.py @@ -2,7 +2,7 @@ from typing import Any from typing_extensions import override -from writer_ai._utils import LazyProxy +from writerai._utils import LazyProxy class RecursiveLazyProxy(LazyProxy[Any]): diff --git a/tests/test_utils/test_typing.py b/tests/test_utils/test_typing.py index 6bc943b1..e0a437df 100644 --- a/tests/test_utils/test_typing.py +++ b/tests/test_utils/test_typing.py @@ -2,7 +2,7 @@ from typing import Generic, TypeVar, cast -from writer_ai._utils import extract_type_var_from_base +from writerai._utils import extract_type_var_from_base _T = TypeVar("_T") _T2 = TypeVar("_T2") diff --git a/tests/utils.py b/tests/utils.py index 73eff2bd..683796ce 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -8,8 +8,8 @@ from datetime import date, datetime from typing_extensions import Literal, get_args, get_origin, assert_type -from writer_ai._types import NoneType -from writer_ai._utils import ( +from writerai._types import NoneType +from writerai._utils import ( is_dict, is_list, is_list_type, @@ -17,8 +17,8 @@ extract_type_arg, is_annotated_type, ) -from writer_ai._compat import PYDANTIC_V2, field_outer_type, get_model_fields -from writer_ai._models import BaseModel +from writerai._compat import PYDANTIC_V2, field_outer_type, get_model_fields +from writerai._models import BaseModel BaseModelT = TypeVar("BaseModelT", bound=BaseModel) From 2bc3520c8871f850bb5796de3daa129c4809cc3c Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Wed, 8 May 2024 14:20:19 +0000 Subject: [PATCH 006/399] feat(api): update via SDK Studio --- .stats.yml | 2 +- CONTRIBUTING.md | 4 +-- README.md | 34 +++++++++---------- pyproject.toml | 4 +-- src/writerai/resources/chat.py | 4 +++ src/writerai/resources/completions.py | 18 +++++----- src/writerai/types/chat_chat_params.py | 2 ++ .../types/completion_create_params.py | 2 +- tests/api_resources/test_chat.py | 34 ++++++++++--------- tests/test_client.py | 16 ++++----- 10 files changed, 64 insertions(+), 56 deletions(-) diff --git a/.stats.yml b/.stats.yml index 9f22043a..701fc274 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 3 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-4a69d32ec3b55feece6cbe0daafff6eb6128e824e162a53ec615ff2ebbf750b6.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-e5ad2fb12fbda084403c1696af9dbe7eeb5f0025134473dea7632339d4d7d00b.yml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 26c16c10..86e75086 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,7 +59,7 @@ If you’d like to use the repository from source, you can either install from g To install via git: ```bash -pip install git+ssh://git@github.com/stainless-sdks/writerai/writer-python.git +pip install git+ssh://git@github.com/stainless-sdks/writer-python.git ``` Alternatively, you can build from source and install the wheel file: @@ -117,7 +117,7 @@ the changes aren't made through the automated pipeline, you may want to make rel ### Publish with a GitHub workflow -You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/stainless-sdks/writerai/writer-python/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. +You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/stainless-sdks/writer-python/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. ### Publish manually diff --git a/README.md b/README.md index 23aab7c5..02b75cce 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The REST API documentation can be found [on dev.writer.com](https://dev.writer.c ```sh # install from this staging repo -pip install git+ssh://git@github.com/stainless-sdks/writerai/writer-python.git +pip install git+ssh://git@github.com/stainless-sdks/writer-python.git ``` > [!NOTE] @@ -38,11 +38,11 @@ client = WriterAI( chat_chat_response = client.chat.chat( messages=[ { - "content": "string", + "content": "Hello!", "role": "user", } ], - model="string", + model="palmyra-x-chat-v2-32k", ) print(chat_chat_response.id) ``` @@ -71,11 +71,11 @@ async def main() -> None: chat_chat_response = await client.chat.chat( messages=[ { - "content": "string", + "content": "Hello!", "role": "user", } ], - model="string", + model="palmyra-x-chat-v2-32k", ) print(chat_chat_response.id) @@ -147,11 +147,11 @@ try: client.chat.chat( messages=[ { - "content": "string", + "content": "Hello!", "role": "user", } ], - model="string", + model="palmyra-x-chat-v2-32k", ) except writerai.APIConnectionError as e: print("The server could not be reached") @@ -198,11 +198,11 @@ client = WriterAI( client.with_options(max_retries=5).chat.chat( messages=[ { - "content": "string", + "content": "Hello!", "role": "user", } ], - model="string", + model="palmyra-x-chat-v2-32k", ) ``` @@ -226,14 +226,14 @@ client = WriterAI( ) # Override per-request: -client.with_options(timeout=5 * 1000).chat.chat( +client.with_options(timeout=5.0).chat.chat( messages=[ { - "content": "string", + "content": "Hello!", "role": "user", } ], - model="string", + model="palmyra-x-chat-v2-32k", ) ``` @@ -275,10 +275,10 @@ from writerai import WriterAI client = WriterAI() response = client.chat.with_raw_response.chat( messages=[{ - "content": "string", + "content": "Hello!", "role": "user", }], - model="string", + model="palmyra-x-chat-v2-32k", ) print(response.headers.get('X-My-Header')) @@ -300,11 +300,11 @@ To stream the response body, use `.with_streaming_response` instead, which requi with client.chat.with_streaming_response.chat( messages=[ { - "content": "string", + "content": "Hello!", "role": "user", } ], - model="string", + model="palmyra-x-chat-v2-32k", ) as response: print(response.headers.get("X-My-Header")) @@ -384,7 +384,7 @@ This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) con We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. -We are keen for your feedback; please open an [issue](https://www.github.com/stainless-sdks/writerai/writer-python/issues) with questions, bugs, or suggestions. +We are keen for your feedback; please open an [issue](https://www.github.com/stainless-sdks/writer-python/issues) with questions, bugs, or suggestions. ## Requirements diff --git a/pyproject.toml b/pyproject.toml index 795b0519..516511cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,8 +39,8 @@ classifiers = [ [project.urls] -Homepage = "https://github.com/stainless-sdks/writerai/writer-python" -Repository = "https://github.com/stainless-sdks/writerai/writer-python" +Homepage = "https://github.com/stainless-sdks/writer-python" +Repository = "https://github.com/stainless-sdks/writer-python" diff --git a/src/writerai/resources/chat.py b/src/writerai/resources/chat.py index 2bd2fa14..609447ef 100644 --- a/src/writerai/resources/chat.py +++ b/src/writerai/resources/chat.py @@ -45,6 +45,7 @@ def chat( max_tokens: int | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, stop: Union[List[str], str] | NotGiven = NOT_GIVEN, + stream: bool | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -75,6 +76,7 @@ def chat( "max_tokens": max_tokens, "n": n, "stop": stop, + "stream": stream, "temperature": temperature, "top_p": top_p, }, @@ -104,6 +106,7 @@ async def chat( max_tokens: int | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, stop: Union[List[str], str] | NotGiven = NOT_GIVEN, + stream: bool | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -134,6 +137,7 @@ async def chat( "max_tokens": max_tokens, "n": n, "stop": stop, + "stream": stream, "temperature": temperature, "top_p": top_p, }, diff --git a/src/writerai/resources/completions.py b/src/writerai/resources/completions.py index ce9ebead..19c6c305 100644 --- a/src/writerai/resources/completions.py +++ b/src/writerai/resources/completions.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import List, overload +from typing import List, Union, overload from typing_extensions import Literal import httpx @@ -50,7 +50,7 @@ def create( best_of: int | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, random_seed: int | NotGiven = NOT_GIVEN, - stop: List[str] | NotGiven = NOT_GIVEN, + stop: Union[List[str], str] | NotGiven = NOT_GIVEN, stream: Literal[False] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, @@ -85,7 +85,7 @@ def create( best_of: int | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, random_seed: int | NotGiven = NOT_GIVEN, - stop: List[str] | NotGiven = NOT_GIVEN, + stop: Union[List[str], str] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -119,7 +119,7 @@ def create( best_of: int | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, random_seed: int | NotGiven = NOT_GIVEN, - stop: List[str] | NotGiven = NOT_GIVEN, + stop: Union[List[str], str] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -152,7 +152,7 @@ def create( best_of: int | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, random_seed: int | NotGiven = NOT_GIVEN, - stop: List[str] | NotGiven = NOT_GIVEN, + stop: Union[List[str], str] | NotGiven = NOT_GIVEN, stream: Literal[False] | Literal[True] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, @@ -206,7 +206,7 @@ async def create( best_of: int | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, random_seed: int | NotGiven = NOT_GIVEN, - stop: List[str] | NotGiven = NOT_GIVEN, + stop: Union[List[str], str] | NotGiven = NOT_GIVEN, stream: Literal[False] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, @@ -241,7 +241,7 @@ async def create( best_of: int | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, random_seed: int | NotGiven = NOT_GIVEN, - stop: List[str] | NotGiven = NOT_GIVEN, + stop: Union[List[str], str] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -275,7 +275,7 @@ async def create( best_of: int | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, random_seed: int | NotGiven = NOT_GIVEN, - stop: List[str] | NotGiven = NOT_GIVEN, + stop: Union[List[str], str] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -308,7 +308,7 @@ async def create( best_of: int | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, random_seed: int | NotGiven = NOT_GIVEN, - stop: List[str] | NotGiven = NOT_GIVEN, + stop: Union[List[str], str] | NotGiven = NOT_GIVEN, stream: Literal[False] | Literal[True] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, diff --git a/src/writerai/types/chat_chat_params.py b/src/writerai/types/chat_chat_params.py index f3a9aa9b..00a3e1c1 100644 --- a/src/writerai/types/chat_chat_params.py +++ b/src/writerai/types/chat_chat_params.py @@ -19,6 +19,8 @@ class ChatChatParams(TypedDict, total=False): stop: Union[List[str], str] + stream: bool + temperature: float top_p: float diff --git a/src/writerai/types/completion_create_params.py b/src/writerai/types/completion_create_params.py index c7580aa7..60c8504d 100644 --- a/src/writerai/types/completion_create_params.py +++ b/src/writerai/types/completion_create_params.py @@ -19,7 +19,7 @@ class CompletionCreateParamsBase(TypedDict, total=False): random_seed: int - stop: List[str] + stop: Union[List[str], str] temperature: float diff --git a/tests/api_resources/test_chat.py b/tests/api_resources/test_chat.py index c1c0f313..9ab90a82 100644 --- a/tests/api_resources/test_chat.py +++ b/tests/api_resources/test_chat.py @@ -22,11 +22,11 @@ def test_method_chat(self, client: WriterAI) -> None: chat = client.chat.chat( messages=[ { - "content": "string", + "content": "Hello!", "role": "user", } ], - model="string", + model="palmyra-x-chat-v2-32k", ) assert_matches_type(ChatChatResponse, chat, path=["response"]) @@ -35,15 +35,16 @@ def test_method_chat_with_all_params(self, client: WriterAI) -> None: chat = client.chat.chat( messages=[ { - "content": "string", + "content": "Hello!", "role": "user", "name": "string", } ], - model="string", + model="palmyra-x-chat-v2-32k", max_tokens=0, n=0, stop=["string", "string", "string"], + stream=True, temperature=0, top_p=0, ) @@ -54,11 +55,11 @@ def test_raw_response_chat(self, client: WriterAI) -> None: response = client.chat.with_raw_response.chat( messages=[ { - "content": "string", + "content": "Hello!", "role": "user", } ], - model="string", + model="palmyra-x-chat-v2-32k", ) assert response.is_closed is True @@ -71,11 +72,11 @@ def test_streaming_response_chat(self, client: WriterAI) -> None: with client.chat.with_streaming_response.chat( messages=[ { - "content": "string", + "content": "Hello!", "role": "user", } ], - model="string", + model="palmyra-x-chat-v2-32k", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -94,11 +95,11 @@ async def test_method_chat(self, async_client: AsyncWriterAI) -> None: chat = await async_client.chat.chat( messages=[ { - "content": "string", + "content": "Hello!", "role": "user", } ], - model="string", + model="palmyra-x-chat-v2-32k", ) assert_matches_type(ChatChatResponse, chat, path=["response"]) @@ -107,15 +108,16 @@ async def test_method_chat_with_all_params(self, async_client: AsyncWriterAI) -> chat = await async_client.chat.chat( messages=[ { - "content": "string", + "content": "Hello!", "role": "user", "name": "string", } ], - model="string", + model="palmyra-x-chat-v2-32k", max_tokens=0, n=0, stop=["string", "string", "string"], + stream=True, temperature=0, top_p=0, ) @@ -126,11 +128,11 @@ async def test_raw_response_chat(self, async_client: AsyncWriterAI) -> None: response = await async_client.chat.with_raw_response.chat( messages=[ { - "content": "string", + "content": "Hello!", "role": "user", } ], - model="string", + model="palmyra-x-chat-v2-32k", ) assert response.is_closed is True @@ -143,11 +145,11 @@ async def test_streaming_response_chat(self, async_client: AsyncWriterAI) -> Non async with async_client.chat.with_streaming_response.chat( messages=[ { - "content": "string", + "content": "Hello!", "role": "user", } ], - model="string", + model="palmyra-x-chat-v2-32k", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" diff --git a/tests/test_client.py b/tests/test_client.py index aaa549f3..c9f387ff 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -726,11 +726,11 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> No dict( messages=[ { - "content": "string", + "content": "Hello!", "role": "user", } ], - model="string", + model="palmyra-x-chat-v2-32k", ), ), cast_to=httpx.Response, @@ -752,11 +752,11 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> Non dict( messages=[ { - "content": "string", + "content": "Hello!", "role": "user", } ], - model="string", + model="palmyra-x-chat-v2-32k", ), ), cast_to=httpx.Response, @@ -1457,11 +1457,11 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) dict( messages=[ { - "content": "string", + "content": "Hello!", "role": "user", } ], - model="string", + model="palmyra-x-chat-v2-32k", ), ), cast_to=httpx.Response, @@ -1483,11 +1483,11 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) dict( messages=[ { - "content": "string", + "content": "Hello!", "role": "user", } ], - model="string", + model="palmyra-x-chat-v2-32k", ), ), cast_to=httpx.Response, From 9662420d21d9cae0dbe278aa0aa82c76be5859ef Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Thu, 9 May 2024 20:05:43 +0000 Subject: [PATCH 007/399] feat(api): update via SDK Studio --- README.md | 8 +- api.md | 4 +- src/writerai/resources/chat.py | 200 +++++++++++++++++- src/writerai/types/__init__.py | 3 +- .../types/{chat_chat_response.py => chat.py} | 4 +- src/writerai/types/chat_chat_params.py | 17 +- src/writerai/types/chat_streaming_data.py | 12 ++ tests/api_resources/test_chat.py | 180 ++++++++++++++-- 8 files changed, 388 insertions(+), 40 deletions(-) rename src/writerai/types/{chat_chat_response.py => chat.py} (83%) create mode 100644 src/writerai/types/chat_streaming_data.py diff --git a/README.md b/README.md index 02b75cce..73bdc9bb 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ client = WriterAI( api_key=os.environ.get("WRITERAI_API_KEY"), ) -chat_chat_response = client.chat.chat( +chat = client.chat.chat( messages=[ { "content": "Hello!", @@ -44,7 +44,7 @@ chat_chat_response = client.chat.chat( ], model="palmyra-x-chat-v2-32k", ) -print(chat_chat_response.id) +print(chat.id) ``` While you can provide an `api_key` keyword argument, @@ -68,7 +68,7 @@ client = AsyncWriterAI( async def main() -> None: - chat_chat_response = await client.chat.chat( + chat = await client.chat.chat( messages=[ { "content": "Hello!", @@ -77,7 +77,7 @@ async def main() -> None: ], model="palmyra-x-chat-v2-32k", ) - print(chat_chat_response.id) + print(chat.id) asyncio.run(main()) diff --git a/api.md b/api.md index 71e75e55..0c42b4dc 100644 --- a/api.md +++ b/api.md @@ -3,12 +3,12 @@ Types: ```python -from writerai.types import ChatChatResponse +from writerai.types import Chat, ChatStreamingData ``` Methods: -- client.chat.chat(\*\*params) -> ChatChatResponse +- client.chat.chat(\*\*params) -> Chat # Completions diff --git a/src/writerai/resources/chat.py b/src/writerai/resources/chat.py index 609447ef..742b950a 100644 --- a/src/writerai/resources/chat.py +++ b/src/writerai/resources/chat.py @@ -2,13 +2,15 @@ from __future__ import annotations -from typing import List, Union, Iterable +from typing import List, Union, Iterable, overload +from typing_extensions import Literal import httpx from ..types import chat_chat_params from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven from .._utils import ( + required_args, maybe_transform, async_maybe_transform, ) @@ -20,10 +22,12 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) +from .._streaming import Stream, AsyncStream +from ..types.chat import Chat from .._base_client import ( make_request_options, ) -from ..types.chat_chat_response import ChatChatResponse +from ..types.chat_streaming_data import ChatStreamingData __all__ = ["ChatResource", "AsyncChatResource"] @@ -37,6 +41,7 @@ def with_raw_response(self) -> ChatResourceWithRawResponse: def with_streaming_response(self) -> ChatResourceWithStreamingResponse: return ChatResourceWithStreamingResponse(self) + @overload def chat( self, *, @@ -45,7 +50,7 @@ def chat( max_tokens: int | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, stop: Union[List[str], str] | NotGiven = NOT_GIVEN, - stream: bool | NotGiven = NOT_GIVEN, + stream: Literal[False] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -54,7 +59,7 @@ def chat( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> ChatChatResponse: + ) -> Chat: """ Create chat completion @@ -67,6 +72,93 @@ def chat( timeout: Override the client-level default timeout for this request, in seconds """ + ... + + @overload + def chat( + self, + *, + messages: Iterable[chat_chat_params.Message], + model: str, + stream: Literal[True], + max_tokens: int | NotGiven = NOT_GIVEN, + n: int | NotGiven = NOT_GIVEN, + stop: Union[List[str], str] | NotGiven = NOT_GIVEN, + temperature: float | NotGiven = NOT_GIVEN, + top_p: float | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Stream[ChatStreamingData]: + """ + Create chat completion + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + def chat( + self, + *, + messages: Iterable[chat_chat_params.Message], + model: str, + stream: bool, + max_tokens: int | NotGiven = NOT_GIVEN, + n: int | NotGiven = NOT_GIVEN, + stop: Union[List[str], str] | NotGiven = NOT_GIVEN, + temperature: float | NotGiven = NOT_GIVEN, + top_p: float | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Chat | Stream[ChatStreamingData]: + """ + Create chat completion + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @required_args(["messages", "model"], ["messages", "model", "stream"]) + def chat( + self, + *, + messages: Iterable[chat_chat_params.Message], + model: str, + max_tokens: int | NotGiven = NOT_GIVEN, + n: int | NotGiven = NOT_GIVEN, + stop: Union[List[str], str] | NotGiven = NOT_GIVEN, + stream: Literal[False] | Literal[True] | NotGiven = NOT_GIVEN, + temperature: float | NotGiven = NOT_GIVEN, + top_p: float | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Chat | Stream[ChatStreamingData]: return self._post( "/v1/chat", body=maybe_transform( @@ -85,7 +177,9 @@ def chat( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ChatChatResponse, + cast_to=Chat, + stream=stream or False, + stream_cls=Stream[ChatStreamingData], ) @@ -98,6 +192,7 @@ def with_raw_response(self) -> AsyncChatResourceWithRawResponse: def with_streaming_response(self) -> AsyncChatResourceWithStreamingResponse: return AsyncChatResourceWithStreamingResponse(self) + @overload async def chat( self, *, @@ -106,7 +201,7 @@ async def chat( max_tokens: int | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, stop: Union[List[str], str] | NotGiven = NOT_GIVEN, - stream: bool | NotGiven = NOT_GIVEN, + stream: Literal[False] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -115,7 +210,7 @@ async def chat( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> ChatChatResponse: + ) -> Chat: """ Create chat completion @@ -128,6 +223,93 @@ async def chat( timeout: Override the client-level default timeout for this request, in seconds """ + ... + + @overload + async def chat( + self, + *, + messages: Iterable[chat_chat_params.Message], + model: str, + stream: Literal[True], + max_tokens: int | NotGiven = NOT_GIVEN, + n: int | NotGiven = NOT_GIVEN, + stop: Union[List[str], str] | NotGiven = NOT_GIVEN, + temperature: float | NotGiven = NOT_GIVEN, + top_p: float | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncStream[ChatStreamingData]: + """ + Create chat completion + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + async def chat( + self, + *, + messages: Iterable[chat_chat_params.Message], + model: str, + stream: bool, + max_tokens: int | NotGiven = NOT_GIVEN, + n: int | NotGiven = NOT_GIVEN, + stop: Union[List[str], str] | NotGiven = NOT_GIVEN, + temperature: float | NotGiven = NOT_GIVEN, + top_p: float | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Chat | AsyncStream[ChatStreamingData]: + """ + Create chat completion + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @required_args(["messages", "model"], ["messages", "model", "stream"]) + async def chat( + self, + *, + messages: Iterable[chat_chat_params.Message], + model: str, + max_tokens: int | NotGiven = NOT_GIVEN, + n: int | NotGiven = NOT_GIVEN, + stop: Union[List[str], str] | NotGiven = NOT_GIVEN, + stream: Literal[False] | Literal[True] | NotGiven = NOT_GIVEN, + temperature: float | NotGiven = NOT_GIVEN, + top_p: float | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Chat | AsyncStream[ChatStreamingData]: return await self._post( "/v1/chat", body=await async_maybe_transform( @@ -146,7 +328,9 @@ async def chat( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ChatChatResponse, + cast_to=Chat, + stream=stream or False, + stream_cls=AsyncStream[ChatStreamingData], ) diff --git a/src/writerai/types/__init__.py b/src/writerai/types/__init__.py index 82199ece..d04f7bf6 100644 --- a/src/writerai/types/__init__.py +++ b/src/writerai/types/__init__.py @@ -2,9 +2,10 @@ from __future__ import annotations +from .chat import Chat as Chat from .completion import Completion as Completion from .streaming_data import StreamingData as StreamingData from .chat_chat_params import ChatChatParams as ChatChatParams -from .chat_chat_response import ChatChatResponse as ChatChatResponse +from .chat_streaming_data import ChatStreamingData as ChatStreamingData from .model_list_response import ModelListResponse as ModelListResponse from .completion_create_params import CompletionCreateParams as CompletionCreateParams diff --git a/src/writerai/types/chat_chat_response.py b/src/writerai/types/chat.py similarity index 83% rename from src/writerai/types/chat_chat_response.py rename to src/writerai/types/chat.py index 6f4bb916..a1782ea4 100644 --- a/src/writerai/types/chat_chat_response.py +++ b/src/writerai/types/chat.py @@ -5,7 +5,7 @@ from .._models import BaseModel -__all__ = ["ChatChatResponse", "Choice", "ChoiceMessage"] +__all__ = ["Chat", "Choice", "ChoiceMessage"] class ChoiceMessage(BaseModel): @@ -20,7 +20,7 @@ class Choice(BaseModel): message: ChoiceMessage -class ChatChatResponse(BaseModel): +class Chat(BaseModel): id: str choices: List[Choice] diff --git a/src/writerai/types/chat_chat_params.py b/src/writerai/types/chat_chat_params.py index 00a3e1c1..986b58d1 100644 --- a/src/writerai/types/chat_chat_params.py +++ b/src/writerai/types/chat_chat_params.py @@ -5,10 +5,10 @@ from typing import List, Union, Iterable from typing_extensions import Literal, Required, TypedDict -__all__ = ["ChatChatParams", "Message"] +__all__ = ["ChatChatParamsBase", "Message", "ChatChatParamsNonStreaming", "ChatChatParamsStreaming"] -class ChatChatParams(TypedDict, total=False): +class ChatChatParamsBase(TypedDict, total=False): messages: Required[Iterable[Message]] model: Required[str] @@ -19,8 +19,6 @@ class ChatChatParams(TypedDict, total=False): stop: Union[List[str], str] - stream: bool - temperature: float top_p: float @@ -32,3 +30,14 @@ class Message(TypedDict, total=False): role: Required[Literal["user", "assistant", "system"]] name: str + + +class ChatChatParamsNonStreaming(ChatChatParamsBase): + stream: Literal[False] + + +class ChatChatParamsStreaming(ChatChatParamsBase): + stream: Required[Literal[True]] + + +ChatChatParams = Union[ChatChatParamsNonStreaming, ChatChatParamsStreaming] diff --git a/src/writerai/types/chat_streaming_data.py b/src/writerai/types/chat_streaming_data.py new file mode 100644 index 00000000..89ae7e83 --- /dev/null +++ b/src/writerai/types/chat_streaming_data.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + + + +from .chat import Chat +from .._models import BaseModel + +__all__ = ["ChatStreamingData"] + + +class ChatStreamingData(BaseModel): + data: Chat diff --git a/tests/api_resources/test_chat.py b/tests/api_resources/test_chat.py index 9ab90a82..0c1b6863 100644 --- a/tests/api_resources/test_chat.py +++ b/tests/api_resources/test_chat.py @@ -9,7 +9,7 @@ from writerai import WriterAI, AsyncWriterAI from tests.utils import assert_matches_type -from writerai.types import ChatChatResponse +from writerai.types import Chat base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -18,7 +18,7 @@ class TestChat: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) @parametrize - def test_method_chat(self, client: WriterAI) -> None: + def test_method_chat_overload_1(self, client: WriterAI) -> None: chat = client.chat.chat( messages=[ { @@ -28,10 +28,10 @@ def test_method_chat(self, client: WriterAI) -> None: ], model="palmyra-x-chat-v2-32k", ) - assert_matches_type(ChatChatResponse, chat, path=["response"]) + assert_matches_type(Chat, chat, path=["response"]) @parametrize - def test_method_chat_with_all_params(self, client: WriterAI) -> None: + def test_method_chat_with_all_params_overload_1(self, client: WriterAI) -> None: chat = client.chat.chat( messages=[ { @@ -44,14 +44,14 @@ def test_method_chat_with_all_params(self, client: WriterAI) -> None: max_tokens=0, n=0, stop=["string", "string", "string"], - stream=True, + stream=False, temperature=0, top_p=0, ) - assert_matches_type(ChatChatResponse, chat, path=["response"]) + assert_matches_type(Chat, chat, path=["response"]) @parametrize - def test_raw_response_chat(self, client: WriterAI) -> None: + def test_raw_response_chat_overload_1(self, client: WriterAI) -> None: response = client.chat.with_raw_response.chat( messages=[ { @@ -65,10 +65,10 @@ def test_raw_response_chat(self, client: WriterAI) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" chat = response.parse() - assert_matches_type(ChatChatResponse, chat, path=["response"]) + assert_matches_type(Chat, chat, path=["response"]) @parametrize - def test_streaming_response_chat(self, client: WriterAI) -> None: + def test_streaming_response_chat_overload_1(self, client: WriterAI) -> None: with client.chat.with_streaming_response.chat( messages=[ { @@ -82,7 +82,78 @@ def test_streaming_response_chat(self, client: WriterAI) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" chat = response.parse() - assert_matches_type(ChatChatResponse, chat, path=["response"]) + assert_matches_type(Chat, chat, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_chat_overload_2(self, client: WriterAI) -> None: + chat_stream = client.chat.chat( + messages=[ + { + "content": "Hello!", + "role": "user", + } + ], + model="palmyra-x-chat-v2-32k", + stream=True, + ) + chat_stream.response.close() + + @parametrize + def test_method_chat_with_all_params_overload_2(self, client: WriterAI) -> None: + chat_stream = client.chat.chat( + messages=[ + { + "content": "Hello!", + "role": "user", + "name": "string", + } + ], + model="palmyra-x-chat-v2-32k", + stream=True, + max_tokens=0, + n=0, + stop=["string", "string", "string"], + temperature=0, + top_p=0, + ) + chat_stream.response.close() + + @parametrize + def test_raw_response_chat_overload_2(self, client: WriterAI) -> None: + response = client.chat.with_raw_response.chat( + messages=[ + { + "content": "Hello!", + "role": "user", + } + ], + model="palmyra-x-chat-v2-32k", + stream=True, + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = response.parse() + stream.close() + + @parametrize + def test_streaming_response_chat_overload_2(self, client: WriterAI) -> None: + with client.chat.with_streaming_response.chat( + messages=[ + { + "content": "Hello!", + "role": "user", + } + ], + model="palmyra-x-chat-v2-32k", + stream=True, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = response.parse() + stream.close() assert cast(Any, response.is_closed) is True @@ -91,7 +162,7 @@ class TestAsyncChat: parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) @parametrize - async def test_method_chat(self, async_client: AsyncWriterAI) -> None: + async def test_method_chat_overload_1(self, async_client: AsyncWriterAI) -> None: chat = await async_client.chat.chat( messages=[ { @@ -101,10 +172,10 @@ async def test_method_chat(self, async_client: AsyncWriterAI) -> None: ], model="palmyra-x-chat-v2-32k", ) - assert_matches_type(ChatChatResponse, chat, path=["response"]) + assert_matches_type(Chat, chat, path=["response"]) @parametrize - async def test_method_chat_with_all_params(self, async_client: AsyncWriterAI) -> None: + async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncWriterAI) -> None: chat = await async_client.chat.chat( messages=[ { @@ -117,14 +188,14 @@ async def test_method_chat_with_all_params(self, async_client: AsyncWriterAI) -> max_tokens=0, n=0, stop=["string", "string", "string"], - stream=True, + stream=False, temperature=0, top_p=0, ) - assert_matches_type(ChatChatResponse, chat, path=["response"]) + assert_matches_type(Chat, chat, path=["response"]) @parametrize - async def test_raw_response_chat(self, async_client: AsyncWriterAI) -> None: + async def test_raw_response_chat_overload_1(self, async_client: AsyncWriterAI) -> None: response = await async_client.chat.with_raw_response.chat( messages=[ { @@ -138,10 +209,10 @@ async def test_raw_response_chat(self, async_client: AsyncWriterAI) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" chat = await response.parse() - assert_matches_type(ChatChatResponse, chat, path=["response"]) + assert_matches_type(Chat, chat, path=["response"]) @parametrize - async def test_streaming_response_chat(self, async_client: AsyncWriterAI) -> None: + async def test_streaming_response_chat_overload_1(self, async_client: AsyncWriterAI) -> None: async with async_client.chat.with_streaming_response.chat( messages=[ { @@ -155,6 +226,77 @@ async def test_streaming_response_chat(self, async_client: AsyncWriterAI) -> Non assert response.http_request.headers.get("X-Stainless-Lang") == "python" chat = await response.parse() - assert_matches_type(ChatChatResponse, chat, path=["response"]) + assert_matches_type(Chat, chat, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_chat_overload_2(self, async_client: AsyncWriterAI) -> None: + chat_stream = await async_client.chat.chat( + messages=[ + { + "content": "Hello!", + "role": "user", + } + ], + model="palmyra-x-chat-v2-32k", + stream=True, + ) + await chat_stream.response.aclose() + + @parametrize + async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncWriterAI) -> None: + chat_stream = await async_client.chat.chat( + messages=[ + { + "content": "Hello!", + "role": "user", + "name": "string", + } + ], + model="palmyra-x-chat-v2-32k", + stream=True, + max_tokens=0, + n=0, + stop=["string", "string", "string"], + temperature=0, + top_p=0, + ) + await chat_stream.response.aclose() + + @parametrize + async def test_raw_response_chat_overload_2(self, async_client: AsyncWriterAI) -> None: + response = await async_client.chat.with_raw_response.chat( + messages=[ + { + "content": "Hello!", + "role": "user", + } + ], + model="palmyra-x-chat-v2-32k", + stream=True, + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = await response.parse() + await stream.close() + + @parametrize + async def test_streaming_response_chat_overload_2(self, async_client: AsyncWriterAI) -> None: + async with async_client.chat.with_streaming_response.chat( + messages=[ + { + "content": "Hello!", + "role": "user", + } + ], + model="palmyra-x-chat-v2-32k", + stream=True, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = await response.parse() + await stream.close() assert cast(Any, response.is_closed) is True From 7eed6970ae3ac53592c1841034f699851ec5c8ee Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Tue, 14 May 2024 16:48:09 +0000 Subject: [PATCH 008/399] feat(api): update via SDK Studio --- .github/workflows/ci.yml | 16 +++------------- README.md | 10 +++++----- SECURITY.md | 27 +++++++++++++++++++++++++++ pyproject.toml | 2 +- requirements-dev.lock | 4 ++-- requirements.lock | 4 ++-- scripts/format | 2 +- scripts/lint | 4 ++++ scripts/test | 1 - src/writerai/_client.py | 12 ++++++------ src/writerai/_models.py | 20 ++++++++++++++++---- tests/test_models.py | 8 ++++---- tests/test_transform.py | 22 ++++++++++++---------- 13 files changed, 83 insertions(+), 49 deletions(-) create mode 100644 SECURITY.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2a2592a..6fcd6aee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,20 +25,10 @@ jobs: RYE_INSTALL_OPTION: '--yes' - name: Install dependencies - run: | - rye sync --all-features - - - name: Run ruff - run: | - rye run check:ruff + run: rye sync --all-features - - name: Run type checking - run: | - rye run typecheck - - - name: Ensure importable - run: | - rye run python -c 'import writerai' + - name: Run lints + run: ./scripts/lint test: name: test runs-on: ubuntu-latest diff --git a/README.md b/README.md index 73bdc9bb..90c47c99 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ from writerai import WriterAI client = WriterAI( # This is the default and can be omitted - api_key=os.environ.get("WRITERAI_API_KEY"), + api_key=os.environ.get("WRITER_API_KEY"), ) chat = client.chat.chat( @@ -49,7 +49,7 @@ print(chat.id) While you can provide an `api_key` keyword argument, we recommend using [python-dotenv](https://pypi.org/project/python-dotenv/) -to add `WRITERAI_API_KEY="My API Key"` to your `.env` file +to add `WRITER_API_KEY="My API Key"` to your `.env` file so that your API Key is not stored in source control. ## Async usage @@ -63,7 +63,7 @@ from writerai import AsyncWriterAI client = AsyncWriterAI( # This is the default and can be omitted - api_key=os.environ.get("WRITERAI_API_KEY"), + api_key=os.environ.get("WRITER_API_KEY"), ) @@ -286,9 +286,9 @@ chat = response.parse() # get the object that `chat.chat()` would have returned print(chat.id) ``` -These methods return an [`APIResponse`](https://github.com/stainless-sdks/tree/main/src/writerai/_response.py) object. +These methods return an [`APIResponse`](https://github.com/stainless-sdks/writer-python/tree/main/src/writerai/_response.py) object. -The async client returns an [`AsyncAPIResponse`](https://github.com/stainless-sdks/tree/main/src/writerai/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. +The async client returns an [`AsyncAPIResponse`](https://github.com/stainless-sdks/writer-python/tree/main/src/writerai/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. #### `.with_streaming_response` diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..c9601ddf --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,27 @@ +# Security Policy + +## Reporting Security Issues + +This SDK is generated by [Stainless Software Inc](http://stainlessapi.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken. + +To report a security issue, please contact the Stainless team at security@stainlessapi.com. + +## Responsible Disclosure + +We appreciate the efforts of security researchers and individuals who help us maintain the security of +SDKs we generate. If you believe you have found a security vulnerability, please adhere to responsible +disclosure practices by allowing us a reasonable amount of time to investigate and address the issue +before making any information public. + +## Reporting Non-SDK Related Security Issues + +If you encounter security issues that are not directly related to SDKs but pertain to the services +or products provided by Writer AI please follow the respective company's security reporting guidelines. + +### Writer AI Terms and Policies + +Please contact dev-feedback@writer.com for any questions or concerns regarding security of our services. + +--- + +Thank you for helping us keep the SDKs and systems they interact with secure. diff --git a/pyproject.toml b/pyproject.toml index 516511cf..42adb3b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -108,7 +108,7 @@ path = "README.md" [[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]] # replace relative links with absolute links pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)' -replacement = '[\1](https://github.com/stainless-sdks/tree/main/\g<2>)' +replacement = '[\1](https://github.com/stainless-sdks/writer-python/tree/main/\g<2>)' [tool.black] line-length = 120 diff --git a/requirements-dev.lock b/requirements-dev.lock index 02f94023..4a97f62a 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -59,9 +59,9 @@ pluggy==1.3.0 # via pytest py==1.11.0 # via pytest -pydantic==2.4.2 +pydantic==2.7.1 # via writerai -pydantic-core==2.10.1 +pydantic-core==2.18.2 # via pydantic pyright==1.1.359 pytest==7.1.1 diff --git a/requirements.lock b/requirements.lock index 81d35ad8..1af2b508 100644 --- a/requirements.lock +++ b/requirements.lock @@ -29,9 +29,9 @@ httpx==0.25.2 idna==3.4 # via anyio # via httpx -pydantic==2.4.2 +pydantic==2.7.1 # via writerai -pydantic-core==2.10.1 +pydantic-core==2.18.2 # via pydantic sniffio==1.3.0 # via anyio diff --git a/scripts/format b/scripts/format index 2a9ea466..667ec2d7 100755 --- a/scripts/format +++ b/scripts/format @@ -4,5 +4,5 @@ set -e cd "$(dirname "$0")/.." +echo "==> Running formatters" rye run format - diff --git a/scripts/lint b/scripts/lint index 0cc68b51..5ecf173a 100755 --- a/scripts/lint +++ b/scripts/lint @@ -4,5 +4,9 @@ set -e cd "$(dirname "$0")/.." +echo "==> Running lints" rye run lint +echo "==> Making sure it imports" +rye run python -c 'import writerai' + diff --git a/scripts/test b/scripts/test index be01d044..b3ace901 100755 --- a/scripts/test +++ b/scripts/test @@ -52,6 +52,5 @@ else echo fi -# Run tests echo "==> Running tests" rye run pytest "$@" diff --git a/src/writerai/_client.py b/src/writerai/_client.py index 2492ff3a..e4338dff 100644 --- a/src/writerai/_client.py +++ b/src/writerai/_client.py @@ -80,13 +80,13 @@ def __init__( ) -> None: """Construct a new synchronous writerai client instance. - This automatically infers the `api_key` argument from the `WRITERAI_API_KEY` environment variable if it is not provided. + This automatically infers the `api_key` argument from the `WRITER_API_KEY` environment variable if it is not provided. """ if api_key is None: - api_key = os.environ.get("WRITERAI_API_KEY") + api_key = os.environ.get("WRITER_API_KEY") if api_key is None: raise WriterAIError( - "The api_key client option must be set either by passing api_key to the client or by setting the WRITERAI_API_KEY environment variable" + "The api_key client option must be set either by passing api_key to the client or by setting the WRITER_API_KEY environment variable" ) self.api_key = api_key @@ -254,13 +254,13 @@ def __init__( ) -> None: """Construct a new async writerai client instance. - This automatically infers the `api_key` argument from the `WRITERAI_API_KEY` environment variable if it is not provided. + This automatically infers the `api_key` argument from the `WRITER_API_KEY` environment variable if it is not provided. """ if api_key is None: - api_key = os.environ.get("WRITERAI_API_KEY") + api_key = os.environ.get("WRITER_API_KEY") if api_key is None: raise WriterAIError( - "The api_key client option must be set either by passing api_key to the client or by setting the WRITERAI_API_KEY environment variable" + "The api_key client option must be set either by passing api_key to the client or by setting the WRITER_API_KEY environment variable" ) self.api_key = api_key diff --git a/src/writerai/_models.py b/src/writerai/_models.py index ff3f54e2..75c68cc7 100644 --- a/src/writerai/_models.py +++ b/src/writerai/_models.py @@ -62,7 +62,7 @@ from ._constants import RAW_RESPONSE_HEADER if TYPE_CHECKING: - from pydantic_core.core_schema import ModelField, ModelFieldsSchema + from pydantic_core.core_schema import ModelField, LiteralSchema, ModelFieldsSchema __all__ = ["BaseModel", "GenericModel"] @@ -251,7 +251,9 @@ def model_dump( exclude_defaults: bool = False, exclude_none: bool = False, round_trip: bool = False, - warnings: bool = True, + warnings: bool | Literal["none", "warn", "error"] = True, + context: dict[str, Any] | None = None, + serialize_as_any: bool = False, ) -> dict[str, Any]: """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump @@ -279,6 +281,10 @@ def model_dump( raise ValueError("round_trip is only supported in Pydantic v2") if warnings != True: raise ValueError("warnings is only supported in Pydantic v2") + if context is not None: + raise ValueError("context is only supported in Pydantic v2") + if serialize_as_any != False: + raise ValueError("serialize_as_any is only supported in Pydantic v2") return super().dict( # pyright: ignore[reportDeprecated] include=include, exclude=exclude, @@ -300,7 +306,9 @@ def model_dump_json( exclude_defaults: bool = False, exclude_none: bool = False, round_trip: bool = False, - warnings: bool = True, + warnings: bool | Literal["none", "warn", "error"] = True, + context: dict[str, Any] | None = None, + serialize_as_any: bool = False, ) -> str: """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump_json @@ -324,6 +332,10 @@ def model_dump_json( raise ValueError("round_trip is only supported in Pydantic v2") if warnings != True: raise ValueError("warnings is only supported in Pydantic v2") + if context is not None: + raise ValueError("context is only supported in Pydantic v2") + if serialize_as_any != False: + raise ValueError("serialize_as_any is only supported in Pydantic v2") return super().json( # type: ignore[reportDeprecated] indent=indent, include=include, @@ -550,7 +562,7 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, field_schema = field["schema"] if field_schema["type"] == "literal": - for entry in field_schema["expected"]: + for entry in cast("LiteralSchema", field_schema)["expected"]: if isinstance(entry, str): mapping[entry] = variant else: diff --git a/tests/test_models.py b/tests/test_models.py index 141feed2..fa64230d 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -31,7 +31,7 @@ class NestedModel(BaseModel): # mismatched types m = NestedModel.construct(nested="hello!") - assert m.nested == "hello!" + assert cast(Any, m.nested) == "hello!" def test_optional_nested_model() -> None: @@ -48,7 +48,7 @@ class NestedModel(BaseModel): # mismatched types m3 = NestedModel.construct(nested={"foo"}) assert isinstance(cast(Any, m3.nested), set) - assert m3.nested == {"foo"} + assert cast(Any, m3.nested) == {"foo"} def test_list_nested_model() -> None: @@ -323,7 +323,7 @@ class Model(BaseModel): assert len(m.items) == 2 assert isinstance(m.items[0], Submodel1) assert m.items[0].level == -1 - assert m.items[1] == 156 + assert cast(Any, m.items[1]) == 156 def test_union_of_lists() -> None: @@ -355,7 +355,7 @@ class Model(BaseModel): assert len(m.items) == 2 assert isinstance(m.items[0], SubModel1) assert m.items[0].level == -1 - assert m.items[1] == 156 + assert cast(Any, m.items[1]) == 156 def test_dict_of_union() -> None: diff --git a/tests/test_transform.py b/tests/test_transform.py index e0265127..9e18f6fe 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -260,20 +260,22 @@ class MyModel(BaseModel): @parametrize @pytest.mark.asyncio async def test_pydantic_model_to_dictionary(use_async: bool) -> None: - assert await transform(MyModel(foo="hi!"), Any, use_async) == {"foo": "hi!"} - assert await transform(MyModel.construct(foo="hi!"), Any, use_async) == {"foo": "hi!"} + assert cast(Any, await transform(MyModel(foo="hi!"), Any, use_async)) == {"foo": "hi!"} + assert cast(Any, await transform(MyModel.construct(foo="hi!"), Any, use_async)) == {"foo": "hi!"} @parametrize @pytest.mark.asyncio async def test_pydantic_empty_model(use_async: bool) -> None: - assert await transform(MyModel.construct(), Any, use_async) == {} + assert cast(Any, await transform(MyModel.construct(), Any, use_async)) == {} @parametrize @pytest.mark.asyncio async def test_pydantic_unknown_field(use_async: bool) -> None: - assert await transform(MyModel.construct(my_untyped_field=True), Any, use_async) == {"my_untyped_field": True} + assert cast(Any, await transform(MyModel.construct(my_untyped_field=True), Any, use_async)) == { + "my_untyped_field": True + } @parametrize @@ -285,7 +287,7 @@ async def test_pydantic_mismatched_types(use_async: bool) -> None: params = await transform(model, Any, use_async) else: params = await transform(model, Any, use_async) - assert params == {"foo": True} + assert cast(Any, params) == {"foo": True} @parametrize @@ -297,7 +299,7 @@ async def test_pydantic_mismatched_object_type(use_async: bool) -> None: params = await transform(model, Any, use_async) else: params = await transform(model, Any, use_async) - assert params == {"foo": {"hello": "world"}} + assert cast(Any, params) == {"foo": {"hello": "world"}} class ModelNestedObjects(BaseModel): @@ -309,7 +311,7 @@ class ModelNestedObjects(BaseModel): async def test_pydantic_nested_objects(use_async: bool) -> None: model = ModelNestedObjects.construct(nested={"foo": "stainless"}) assert isinstance(model.nested, MyModel) - assert await transform(model, Any, use_async) == {"nested": {"foo": "stainless"}} + assert cast(Any, await transform(model, Any, use_async)) == {"nested": {"foo": "stainless"}} class ModelWithDefaultField(BaseModel): @@ -325,19 +327,19 @@ async def test_pydantic_default_field(use_async: bool) -> None: model = ModelWithDefaultField.construct() assert model.with_none_default is None assert model.with_str_default == "foo" - assert await transform(model, Any, use_async) == {} + assert cast(Any, await transform(model, Any, use_async)) == {} # should be included when the default value is explicitly given model = ModelWithDefaultField.construct(with_none_default=None, with_str_default="foo") assert model.with_none_default is None assert model.with_str_default == "foo" - assert await transform(model, Any, use_async) == {"with_none_default": None, "with_str_default": "foo"} + assert cast(Any, await transform(model, Any, use_async)) == {"with_none_default": None, "with_str_default": "foo"} # should be included when a non-default value is explicitly given model = ModelWithDefaultField.construct(with_none_default="bar", with_str_default="baz") assert model.with_none_default == "bar" assert model.with_str_default == "baz" - assert await transform(model, Any, use_async) == {"with_none_default": "bar", "with_str_default": "baz"} + assert cast(Any, await transform(model, Any, use_async)) == {"with_none_default": "bar", "with_str_default": "baz"} class TypedDictIterableUnion(TypedDict): From 2e8137bef11b11ce80fea18d673feec7ed8c1e6f Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Tue, 14 May 2024 18:58:52 +0000 Subject: [PATCH 009/399] feat(api): update via SDK Studio --- CONTRIBUTING.md | 2 +- LICENSE | 2 +- README.md | 72 +++---- SECURITY.md | 4 +- api.md | 12 +- mypy.ini | 2 +- pyproject.toml | 12 +- requirements-dev.lock | 12 +- requirements.lock | 12 +- scripts/lint | 2 +- src/{writerai => writer}/__init__.py | 24 +-- src/{writerai => writer}/_base_client.py | 2 +- src/{writerai => writer}/_client.py | 58 +++--- src/{writerai => writer}/_compat.py | 0 src/{writerai => writer}/_constants.py | 0 src/{writerai => writer}/_exceptions.py | 4 +- src/{writerai => writer}/_files.py | 0 src/{writerai => writer}/_models.py | 0 src/{writerai => writer}/_qs.py | 0 src/{writerai => writer}/_resource.py | 10 +- src/{writerai => writer}/_response.py | 12 +- src/{writerai => writer}/_streaming.py | 6 +- src/{writerai => writer}/_types.py | 2 +- src/{writerai => writer}/_utils/__init__.py | 0 src/{writerai => writer}/_utils/_logs.py | 6 +- src/{writerai => writer}/_utils/_proxy.py | 0 src/{writerai => writer}/_utils/_streams.py | 0 src/{writerai => writer}/_utils/_sync.py | 0 src/{writerai => writer}/_utils/_transform.py | 0 src/{writerai => writer}/_utils/_typing.py | 0 src/{writerai => writer}/_utils/_utils.py | 0 src/{writerai => writer}/_version.py | 2 +- src/writer/lib/.keep | 4 + src/{writerai => writer}/py.typed | 0 .../resources/__init__.py | 0 src/{writerai => writer}/resources/chat.py | 0 .../resources/completions.py | 0 src/{writerai => writer}/resources/models.py | 0 src/{writerai => writer}/types/__init__.py | 0 src/{writerai => writer}/types/chat.py | 0 .../types/chat_chat_params.py | 0 .../types/chat_streaming_data.py | 0 src/{writerai => writer}/types/completion.py | 0 .../types/completion_create_params.py | 0 .../types/model_list_response.py | 0 .../types/streaming_data.py | 0 tests/api_resources/test_chat.py | 36 ++-- tests/api_resources/test_completions.py | 36 ++-- tests/api_resources/test_models.py | 16 +- tests/conftest.py | 12 +- tests/test_client.py | 177 +++++++++--------- tests/test_deepcopy.py | 2 +- tests/test_extract_files.py | 4 +- tests/test_files.py | 2 +- tests/test_models.py | 6 +- tests/test_qs.py | 2 +- tests/test_required_args.py | 2 +- tests/test_response.py | 30 +-- tests/test_streaming.py | 32 ++-- tests/test_transform.py | 8 +- tests/test_utils/test_proxy.py | 2 +- tests/test_utils/test_typing.py | 2 +- tests/utils.py | 8 +- 63 files changed, 313 insertions(+), 326 deletions(-) rename src/{writerai => writer}/__init__.py (85%) rename src/{writerai => writer}/_base_client.py (99%) rename src/{writerai => writer}/_client.py (91%) rename src/{writerai => writer}/_compat.py (100%) rename src/{writerai => writer}/_constants.py (100%) rename src/{writerai => writer}/_exceptions.py (98%) rename src/{writerai => writer}/_files.py (100%) rename src/{writerai => writer}/_models.py (100%) rename src/{writerai => writer}/_qs.py (100%) rename src/{writerai => writer}/_resource.py (81%) rename src/{writerai => writer}/_response.py (98%) rename src/{writerai => writer}/_streaming.py (99%) rename src/{writerai => writer}/_types.py (99%) rename src/{writerai => writer}/_utils/__init__.py (100%) rename src/{writerai => writer}/_utils/_logs.py (71%) rename src/{writerai => writer}/_utils/_proxy.py (100%) rename src/{writerai => writer}/_utils/_streams.py (100%) rename src/{writerai => writer}/_utils/_sync.py (100%) rename src/{writerai => writer}/_utils/_transform.py (100%) rename src/{writerai => writer}/_utils/_typing.py (100%) rename src/{writerai => writer}/_utils/_utils.py (100%) rename src/{writerai => writer}/_version.py (83%) create mode 100644 src/writer/lib/.keep rename src/{writerai => writer}/py.typed (100%) rename src/{writerai => writer}/resources/__init__.py (100%) rename src/{writerai => writer}/resources/chat.py (100%) rename src/{writerai => writer}/resources/completions.py (100%) rename src/{writerai => writer}/resources/models.py (100%) rename src/{writerai => writer}/types/__init__.py (100%) rename src/{writerai => writer}/types/chat.py (100%) rename src/{writerai => writer}/types/chat_chat_params.py (100%) rename src/{writerai => writer}/types/chat_streaming_data.py (100%) rename src/{writerai => writer}/types/completion.py (100%) rename src/{writerai => writer}/types/completion_create_params.py (100%) rename src/{writerai => writer}/types/model_list_response.py (100%) rename src/{writerai => writer}/types/streaming_data.py (100%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 86e75086..57685f7e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,7 +32,7 @@ $ pip install -r requirements-dev.lock ## Modifying/Adding code Most of the SDK is generated code, and any modified code will be overridden on the next generation. The -`src/writerai/lib/` and `examples/` directories are exceptions and will never be overridden. +`src/writer/lib/` and `examples/` directories are exceptions and will never be overridden. ## Adding and running examples diff --git a/LICENSE b/LICENSE index 38aac4e7..8dbbf687 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2024 Writer AI + Copyright 2024 Writer 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 90c47c99..23bd7b66 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# Writer AI Python API library +# Writer Python API library -[![PyPI version](https://img.shields.io/pypi/v/writerai.svg)](https://pypi.org/project/writerai/) +[![PyPI version](https://img.shields.io/pypi/v/writer-sdk.svg)](https://pypi.org/project/writer-sdk/) -The Writer AI Python library provides convenient access to the Writer AI REST API from any Python 3.7+ +The Writer Python library provides convenient access to the Writer REST API from any Python 3.7+ 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). @@ -20,7 +20,7 @@ pip install git+ssh://git@github.com/stainless-sdks/writer-python.git ``` > [!NOTE] -> Once this package is [published to PyPI](https://app.stainlessapi.com/docs/guides/publish), this will become: `pip install --pre writerai` +> Once this package is [published to PyPI](https://app.stainlessapi.com/docs/guides/publish), this will become: `pip install --pre writer-sdk` ## Usage @@ -28,9 +28,9 @@ The full API of this library can be found in [api.md](api.md). ```python import os -from writerai import WriterAI +from writer import Writer -client = WriterAI( +client = Writer( # This is the default and can be omitted api_key=os.environ.get("WRITER_API_KEY"), ) @@ -54,14 +54,14 @@ so that your API Key is not stored in source control. ## Async usage -Simply import `AsyncWriterAI` instead of `WriterAI` and use `await` with each API call: +Simply import `AsyncWriter` instead of `Writer` and use `await` with each API call: ```python import os import asyncio -from writerai import AsyncWriterAI +from writer import AsyncWriter -client = AsyncWriterAI( +client = AsyncWriter( # This is the default and can be omitted api_key=os.environ.get("WRITER_API_KEY"), ) @@ -90,9 +90,9 @@ Functionality between the synchronous and asynchronous clients is otherwise iden We provide support for streaming responses using Server Side Events (SSE). ```python -from writerai import WriterAI +from writer import Writer -client = WriterAI() +client = Writer() stream = client.completions.create( model="palmyra-x-v2", @@ -106,9 +106,9 @@ for completion in stream: The async client uses the exact same interface. ```python -from writerai import AsyncWriterAI +from writer import AsyncWriter -client = AsyncWriterAI() +client = AsyncWriter() stream = await client.completions.create( model="palmyra-x-v2", @@ -130,18 +130,18 @@ Typed requests and responses provide autocomplete and documentation within your ## Handling errors -When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `writerai.APIConnectionError` is raised. +When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `writer.APIConnectionError` is raised. When the API returns a non-success status code (that is, 4xx or 5xx -response), a subclass of `writerai.APIStatusError` is raised, containing `status_code` and `response` properties. +response), a subclass of `writer.APIStatusError` is raised, containing `status_code` and `response` properties. -All errors inherit from `writerai.APIError`. +All errors inherit from `writer.APIError`. ```python -import writerai -from writerai import WriterAI +import writer +from writer import Writer -client = WriterAI() +client = Writer() try: client.chat.chat( @@ -153,12 +153,12 @@ try: ], model="palmyra-x-chat-v2-32k", ) -except writerai.APIConnectionError as e: +except writer.APIConnectionError as e: print("The server could not be reached") print(e.__cause__) # an underlying Exception, likely raised within httpx. -except writerai.RateLimitError as e: +except writer.RateLimitError as e: print("A 429 status code was received; we should back off a bit.") -except writerai.APIStatusError as e: +except writer.APIStatusError as e: print("Another non-200-range status code was received") print(e.status_code) print(e.response) @@ -186,10 +186,10 @@ Connection errors (for example, due to a network connectivity problem), 408 Requ You can use the `max_retries` option to configure or disable retry settings: ```python -from writerai import WriterAI +from writer import Writer # Configure the default for all requests: -client = WriterAI( +client = Writer( # default is 2 max_retries=0, ) @@ -212,16 +212,16 @@ By default requests time out after 1 minute. You can configure this with a `time which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/#fine-tuning-the-configuration) object: ```python -from writerai import WriterAI +from writer import Writer # Configure the default for all requests: -client = WriterAI( +client = Writer( # 20 seconds (default is 1 minute) timeout=20.0, ) # More granular control: -client = WriterAI( +client = Writer( timeout=httpx.Timeout(60.0, read=5.0, write=10.0, connect=2.0), ) @@ -247,10 +247,10 @@ Note that requests that time out are [retried twice by default](#retries). We use the standard library [`logging`](https://docs.python.org/3/library/logging.html) module. -You can enable logging by setting the environment variable `WRITERAI_LOG` to `debug`. +You can enable logging by setting the environment variable `WRITER_LOG` to `debug`. ```shell -$ export WRITERAI_LOG=debug +$ export WRITER_LOG=debug ``` ### How to tell whether `None` means `null` or missing @@ -270,9 +270,9 @@ if response.my_field is None: The "raw" Response object can be accessed by prefixing `.with_raw_response.` to any HTTP method call, e.g., ```py -from writerai import WriterAI +from writer import Writer -client = WriterAI() +client = Writer() response = client.chat.with_raw_response.chat( messages=[{ "content": "Hello!", @@ -286,9 +286,9 @@ chat = response.parse() # get the object that `chat.chat()` would have returned print(chat.id) ``` -These methods return an [`APIResponse`](https://github.com/stainless-sdks/writer-python/tree/main/src/writerai/_response.py) object. +These methods return an [`APIResponse`](https://github.com/stainless-sdks/writer-python/tree/main/src/writer/_response.py) object. -The async client returns an [`AsyncAPIResponse`](https://github.com/stainless-sdks/writer-python/tree/main/src/writerai/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. +The async client returns an [`AsyncAPIResponse`](https://github.com/stainless-sdks/writer-python/tree/main/src/writer/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. #### `.with_streaming_response` @@ -358,10 +358,10 @@ You can directly override the [httpx client](https://www.python-httpx.org/api/#c - Additional [advanced](https://www.python-httpx.org/advanced/#client-instances) functionality ```python -from writerai import WriterAI, DefaultHttpxClient +from writer import Writer, DefaultHttpxClient -client = WriterAI( - # Or use the `WRITERAI_BASE_URL` env var +client = Writer( + # Or use the `WRITER_BASE_URL` env var base_url="http://my.test.server.example.com:8083", http_client=DefaultHttpxClient( proxies="http://my.test.proxy.example.com", diff --git a/SECURITY.md b/SECURITY.md index c9601ddf..f08c9053 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -16,9 +16,9 @@ before making any information public. ## Reporting Non-SDK Related Security Issues If you encounter security issues that are not directly related to SDKs but pertain to the services -or products provided by Writer AI please follow the respective company's security reporting guidelines. +or products provided by Writer please follow the respective company's security reporting guidelines. -### Writer AI Terms and Policies +### Writer Terms and Policies Please contact dev-feedback@writer.com for any questions or concerns regarding security of our services. diff --git a/api.md b/api.md index 0c42b4dc..190d95ac 100644 --- a/api.md +++ b/api.md @@ -3,33 +3,33 @@ Types: ```python -from writerai.types import Chat, ChatStreamingData +from writer.types import Chat, ChatStreamingData ``` Methods: -- client.chat.chat(\*\*params) -> Chat +- client.chat.chat(\*\*params) -> Chat # Completions Types: ```python -from writerai.types import Completion, StreamingData +from writer.types import Completion, StreamingData ``` Methods: -- client.completions.create(\*\*params) -> Completion +- client.completions.create(\*\*params) -> Completion # Models Types: ```python -from writerai.types import ModelListResponse +from writer.types import ModelListResponse ``` Methods: -- client.models.list() -> ModelListResponse +- client.models.list() -> ModelListResponse diff --git a/mypy.ini b/mypy.ini index 1ece7ab6..20d1f7ec 100644 --- a/mypy.ini +++ b/mypy.ini @@ -5,7 +5,7 @@ show_error_codes = True # Exclude _files.py because mypy isn't smart enough to apply # the correct type narrowing and as this is an internal module # it's fine to just use Pyright. -exclude = ^(src/writerai/_files\.py|_dev/.*\.py)$ +exclude = ^(src/writer/_files\.py|_dev/.*\.py)$ strict_equality = True implicit_reexport = True diff --git a/pyproject.toml b/pyproject.toml index 42adb3b6..c9702fd8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,11 @@ [project] -name = "writerai" +name = "writer-sdk" version = "0.0.1-alpha.0" -description = "The official Python library for the writerai API" +description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" authors = [ -{ name = "Writer AI", email = "dev-feedback@writer.com" }, +{ name = "Writer", email = "dev-feedback@writer.com" }, ] dependencies = [ "httpx>=0.23.0, <1", @@ -84,7 +84,7 @@ typecheck = { chain = [ "typecheck:mypy" ]} "typecheck:pyright" = "pyright" -"typecheck:verify-types" = "pyright --verifytypes writerai --ignoreexternal" +"typecheck:verify-types" = "pyright --verifytypes writer --ignoreexternal" "typecheck:mypy" = "mypy ." [build-system] @@ -97,7 +97,7 @@ include = [ ] [tool.hatch.build.targets.wheel] -packages = ["src/writerai"] +packages = ["src/writer"] [tool.hatch.metadata.hooks.fancy-pypi-readme] content-type = "text/markdown" @@ -187,7 +187,7 @@ length-sort = true length-sort-straight = true combine-as-imports = true extra-standard-library = ["typing_extensions"] -known-first-party = ["writerai", "tests"] +known-first-party = ["writer", "tests"] [tool.ruff.per-file-ignores] "bin/**.py" = ["T201", "T203"] diff --git a/requirements-dev.lock b/requirements-dev.lock index 4a97f62a..752668e7 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -12,7 +12,7 @@ annotated-types==0.6.0 # via pydantic anyio==4.1.0 # via httpx - # via writerai + # via writer-sdk argcomplete==3.1.2 # via nox attrs==23.1.0 @@ -26,7 +26,7 @@ dirty-equals==0.6.0 distlib==0.3.7 # via virtualenv distro==1.8.0 - # via writerai + # via writer-sdk exceptiongroup==1.1.3 # via anyio filelock==3.12.4 @@ -37,7 +37,7 @@ httpcore==1.0.2 # via httpx httpx==0.25.2 # via respx - # via writerai + # via writer-sdk idna==3.4 # via anyio # via httpx @@ -60,7 +60,7 @@ pluggy==1.3.0 py==1.11.0 # via pytest pydantic==2.7.1 - # via writerai + # via writer-sdk pydantic-core==2.18.2 # via pydantic pyright==1.1.359 @@ -80,7 +80,7 @@ six==1.16.0 sniffio==1.3.0 # via anyio # via httpx - # via writerai + # via writer-sdk time-machine==2.9.0 tomli==2.0.1 # via mypy @@ -89,7 +89,7 @@ typing-extensions==4.8.0 # via mypy # via pydantic # via pydantic-core - # via writerai + # via writer-sdk virtualenv==20.24.5 # via nox zipp==3.17.0 diff --git a/requirements.lock b/requirements.lock index 1af2b508..efb1c1b4 100644 --- a/requirements.lock +++ b/requirements.lock @@ -12,12 +12,12 @@ annotated-types==0.6.0 # via pydantic anyio==4.1.0 # via httpx - # via writerai + # via writer-sdk certifi==2023.7.22 # via httpcore # via httpx distro==1.8.0 - # via writerai + # via writer-sdk exceptiongroup==1.1.3 # via anyio h11==0.14.0 @@ -25,19 +25,19 @@ h11==0.14.0 httpcore==1.0.2 # via httpx httpx==0.25.2 - # via writerai + # via writer-sdk idna==3.4 # via anyio # via httpx pydantic==2.7.1 - # via writerai + # via writer-sdk pydantic-core==2.18.2 # via pydantic sniffio==1.3.0 # via anyio # via httpx - # via writerai + # via writer-sdk typing-extensions==4.8.0 # via pydantic # via pydantic-core - # via writerai + # via writer-sdk diff --git a/scripts/lint b/scripts/lint index 5ecf173a..3260e8a0 100755 --- a/scripts/lint +++ b/scripts/lint @@ -8,5 +8,5 @@ echo "==> Running lints" rye run lint echo "==> Making sure it imports" -rye run python -c 'import writerai' +rye run python -c 'import writer' diff --git a/src/writerai/__init__.py b/src/writer/__init__.py similarity index 85% rename from src/writerai/__init__.py rename to src/writer/__init__.py index 2c51f189..1b352daa 100644 --- a/src/writerai/__init__.py +++ b/src/writer/__init__.py @@ -3,26 +3,16 @@ from . import types from ._types import NOT_GIVEN, NoneType, NotGiven, Transport, ProxiesTypes from ._utils import file_from_path -from ._client import ( - Client, - Stream, - Timeout, - WriterAI, - Transport, - AsyncClient, - AsyncStream, - AsyncWriterAI, - RequestOptions, -) +from ._client import Client, Stream, Writer, Timeout, Transport, AsyncClient, AsyncStream, AsyncWriter, RequestOptions from ._models import BaseModel from ._version import __title__, __version__ from ._response import APIResponse as APIResponse, AsyncAPIResponse as AsyncAPIResponse from ._constants import DEFAULT_TIMEOUT, DEFAULT_MAX_RETRIES, DEFAULT_CONNECTION_LIMITS from ._exceptions import ( APIError, + WriterError, ConflictError, NotFoundError, - WriterAIError, APIStatusError, RateLimitError, APITimeoutError, @@ -46,7 +36,7 @@ "ProxiesTypes", "NotGiven", "NOT_GIVEN", - "WriterAIError", + "WriterError", "APIError", "APIStatusError", "APITimeoutError", @@ -66,8 +56,8 @@ "AsyncClient", "Stream", "AsyncStream", - "WriterAI", - "AsyncWriterAI", + "Writer", + "AsyncWriter", "file_from_path", "BaseModel", "DEFAULT_TIMEOUT", @@ -82,12 +72,12 @@ # Update the __module__ attribute for exported symbols so that # error messages point to this module instead of the module # it was originally defined in, e.g. -# writerai._exceptions.NotFoundError -> writerai.NotFoundError +# writer._exceptions.NotFoundError -> writer.NotFoundError __locals = locals() for __name in __all__: if not __name.startswith("__"): try: - __locals[__name].__module__ = "writerai" + __locals[__name].__module__ = "writer" except (TypeError, AttributeError): # Some of our exported symbols are builtins which we can't set attributes for. pass diff --git a/src/writerai/_base_client.py b/src/writer/_base_client.py similarity index 99% rename from src/writerai/_base_client.py rename to src/writer/_base_client.py index f0896225..633afbb6 100644 --- a/src/writerai/_base_client.py +++ b/src/writer/_base_client.py @@ -361,7 +361,7 @@ def __init__( if max_retries is None: # pyright: ignore[reportUnnecessaryComparison] raise TypeError( - "max_retries cannot be None. If you want to disable retries, pass `0`; if you want unlimited retries, pass `math.inf` or a very high number; if you want the default behavior, pass `writerai.DEFAULT_MAX_RETRIES`" + "max_retries cannot be None. If you want to disable retries, pass `0`; if you want unlimited retries, pass `math.inf` or a very high number; if you want the default behavior, pass `writer-sdk.DEFAULT_MAX_RETRIES`" ) def _enforce_trailing_slash(self, url: URL) -> URL: diff --git a/src/writerai/_client.py b/src/writer/_client.py similarity index 91% rename from src/writerai/_client.py rename to src/writer/_client.py index e4338dff..f5c3c805 100644 --- a/src/writerai/_client.py +++ b/src/writer/_client.py @@ -25,7 +25,7 @@ ) from ._version import __version__ from ._streaming import Stream as Stream, AsyncStream as AsyncStream -from ._exceptions import WriterAIError, APIStatusError +from ._exceptions import WriterError, APIStatusError from ._base_client import ( DEFAULT_MAX_RETRIES, SyncAPIClient, @@ -38,19 +38,19 @@ "ProxiesTypes", "RequestOptions", "resources", - "WriterAI", - "AsyncWriterAI", + "Writer", + "AsyncWriter", "Client", "AsyncClient", ] -class WriterAI(SyncAPIClient): +class Writer(SyncAPIClient): chat: resources.ChatResource completions: resources.CompletionsResource models: resources.ModelsResource - with_raw_response: WriterAIWithRawResponse - with_streaming_response: WriterAIWithStreamedResponse + with_raw_response: WriterWithRawResponse + with_streaming_response: WriterWithStreamedResponse # client options api_key: str @@ -78,20 +78,20 @@ def __init__( # part of our public interface in the future. _strict_response_validation: bool = False, ) -> None: - """Construct a new synchronous writerai client instance. + """Construct a new synchronous writer client instance. This automatically infers the `api_key` argument from the `WRITER_API_KEY` environment variable if it is not provided. """ if api_key is None: api_key = os.environ.get("WRITER_API_KEY") if api_key is None: - raise WriterAIError( + raise WriterError( "The api_key client option must be set either by passing api_key to the client or by setting the WRITER_API_KEY environment variable" ) self.api_key = api_key if base_url is None: - base_url = os.environ.get("WRITERAI_BASE_URL") + base_url = os.environ.get("WRITER_BASE_URL") if base_url is None: base_url = f"https://api.qordobadev.com" @@ -111,8 +111,8 @@ def __init__( self.chat = resources.ChatResource(self) self.completions = resources.CompletionsResource(self) self.models = resources.ModelsResource(self) - self.with_raw_response = WriterAIWithRawResponse(self) - self.with_streaming_response = WriterAIWithStreamedResponse(self) + self.with_raw_response = WriterWithRawResponse(self) + self.with_streaming_response = WriterWithStreamedResponse(self) @property @override @@ -219,12 +219,12 @@ def _make_status_error( return APIStatusError(err_msg, response=response, body=body) -class AsyncWriterAI(AsyncAPIClient): +class AsyncWriter(AsyncAPIClient): chat: resources.AsyncChatResource completions: resources.AsyncCompletionsResource models: resources.AsyncModelsResource - with_raw_response: AsyncWriterAIWithRawResponse - with_streaming_response: AsyncWriterAIWithStreamedResponse + with_raw_response: AsyncWriterWithRawResponse + with_streaming_response: AsyncWriterWithStreamedResponse # client options api_key: str @@ -252,20 +252,20 @@ def __init__( # part of our public interface in the future. _strict_response_validation: bool = False, ) -> None: - """Construct a new async writerai client instance. + """Construct a new async writer client instance. This automatically infers the `api_key` argument from the `WRITER_API_KEY` environment variable if it is not provided. """ if api_key is None: api_key = os.environ.get("WRITER_API_KEY") if api_key is None: - raise WriterAIError( + raise WriterError( "The api_key client option must be set either by passing api_key to the client or by setting the WRITER_API_KEY environment variable" ) self.api_key = api_key if base_url is None: - base_url = os.environ.get("WRITERAI_BASE_URL") + base_url = os.environ.get("WRITER_BASE_URL") if base_url is None: base_url = f"https://api.qordobadev.com" @@ -285,8 +285,8 @@ def __init__( self.chat = resources.AsyncChatResource(self) self.completions = resources.AsyncCompletionsResource(self) self.models = resources.AsyncModelsResource(self) - self.with_raw_response = AsyncWriterAIWithRawResponse(self) - self.with_streaming_response = AsyncWriterAIWithStreamedResponse(self) + self.with_raw_response = AsyncWriterWithRawResponse(self) + self.with_streaming_response = AsyncWriterWithStreamedResponse(self) @property @override @@ -393,34 +393,34 @@ def _make_status_error( return APIStatusError(err_msg, response=response, body=body) -class WriterAIWithRawResponse: - def __init__(self, client: WriterAI) -> None: +class WriterWithRawResponse: + def __init__(self, client: Writer) -> None: self.chat = resources.ChatResourceWithRawResponse(client.chat) self.completions = resources.CompletionsResourceWithRawResponse(client.completions) self.models = resources.ModelsResourceWithRawResponse(client.models) -class AsyncWriterAIWithRawResponse: - def __init__(self, client: AsyncWriterAI) -> None: +class AsyncWriterWithRawResponse: + def __init__(self, client: AsyncWriter) -> None: self.chat = resources.AsyncChatResourceWithRawResponse(client.chat) self.completions = resources.AsyncCompletionsResourceWithRawResponse(client.completions) self.models = resources.AsyncModelsResourceWithRawResponse(client.models) -class WriterAIWithStreamedResponse: - def __init__(self, client: WriterAI) -> None: +class WriterWithStreamedResponse: + def __init__(self, client: Writer) -> None: self.chat = resources.ChatResourceWithStreamingResponse(client.chat) self.completions = resources.CompletionsResourceWithStreamingResponse(client.completions) self.models = resources.ModelsResourceWithStreamingResponse(client.models) -class AsyncWriterAIWithStreamedResponse: - def __init__(self, client: AsyncWriterAI) -> None: +class AsyncWriterWithStreamedResponse: + def __init__(self, client: AsyncWriter) -> None: self.chat = resources.AsyncChatResourceWithStreamingResponse(client.chat) self.completions = resources.AsyncCompletionsResourceWithStreamingResponse(client.completions) self.models = resources.AsyncModelsResourceWithStreamingResponse(client.models) -Client = WriterAI +Client = Writer -AsyncClient = AsyncWriterAI +AsyncClient = AsyncWriter diff --git a/src/writerai/_compat.py b/src/writer/_compat.py similarity index 100% rename from src/writerai/_compat.py rename to src/writer/_compat.py diff --git a/src/writerai/_constants.py b/src/writer/_constants.py similarity index 100% rename from src/writerai/_constants.py rename to src/writer/_constants.py diff --git a/src/writerai/_exceptions.py b/src/writer/_exceptions.py similarity index 98% rename from src/writerai/_exceptions.py rename to src/writer/_exceptions.py index 2ceb96a7..684af996 100644 --- a/src/writerai/_exceptions.py +++ b/src/writer/_exceptions.py @@ -18,11 +18,11 @@ ] -class WriterAIError(Exception): +class WriterError(Exception): pass -class APIError(WriterAIError): +class APIError(WriterError): message: str request: httpx.Request diff --git a/src/writerai/_files.py b/src/writer/_files.py similarity index 100% rename from src/writerai/_files.py rename to src/writer/_files.py diff --git a/src/writerai/_models.py b/src/writer/_models.py similarity index 100% rename from src/writerai/_models.py rename to src/writer/_models.py diff --git a/src/writerai/_qs.py b/src/writer/_qs.py similarity index 100% rename from src/writerai/_qs.py rename to src/writer/_qs.py diff --git a/src/writerai/_resource.py b/src/writer/_resource.py similarity index 81% rename from src/writerai/_resource.py rename to src/writer/_resource.py index bc6c2634..b62a9f28 100644 --- a/src/writerai/_resource.py +++ b/src/writer/_resource.py @@ -8,13 +8,13 @@ import anyio if TYPE_CHECKING: - from ._client import WriterAI, AsyncWriterAI + from ._client import Writer, AsyncWriter class SyncAPIResource: - _client: WriterAI + _client: Writer - def __init__(self, client: WriterAI) -> None: + def __init__(self, client: Writer) -> None: self._client = client self._get = client.get self._post = client.post @@ -28,9 +28,9 @@ def _sleep(self, seconds: float) -> None: class AsyncAPIResource: - _client: AsyncWriterAI + _client: AsyncWriter - def __init__(self, client: AsyncWriterAI) -> None: + def __init__(self, client: AsyncWriter) -> None: self._client = client self._get = client.get self._post = client.post diff --git a/src/writerai/_response.py b/src/writer/_response.py similarity index 98% rename from src/writerai/_response.py rename to src/writer/_response.py index 7739bb85..ffbdee5f 100644 --- a/src/writerai/_response.py +++ b/src/writer/_response.py @@ -29,7 +29,7 @@ from ._models import BaseModel, is_basemodel from ._constants import RAW_RESPONSE_HEADER, OVERRIDE_CAST_TO_HEADER from ._streaming import Stream, AsyncStream, is_stream_class_type, extract_stream_chunk_type -from ._exceptions import WriterAIError, APIResponseValidationError +from ._exceptions import WriterError, APIResponseValidationError if TYPE_CHECKING: from ._models import FinalRequestOptions @@ -203,7 +203,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: return cast(R, response) if inspect.isclass(origin) and not issubclass(origin, BaseModel) and issubclass(origin, pydantic.BaseModel): - raise TypeError("Pydantic models must subclass our base model type, e.g. `from writerai import BaseModel`") + raise TypeError("Pydantic models must subclass our base model type, e.g. `from writer import BaseModel`") if ( cast_to is not object @@ -271,7 +271,7 @@ def parse(self, *, to: type[_T] | None = None) -> R | _T: the `to` argument, e.g. ```py - from writerai import BaseModel + from writer import BaseModel class MyModel(BaseModel): @@ -375,7 +375,7 @@ async def parse(self, *, to: type[_T] | None = None) -> R | _T: the `to` argument, e.g. ```py - from writerai import BaseModel + from writer import BaseModel class MyModel(BaseModel): @@ -546,11 +546,11 @@ async def stream_to_file( class MissingStreamClassError(TypeError): def __init__(self) -> None: super().__init__( - "The `stream` argument was set to `True` but the `stream_cls` argument was not given. See `writerai._streaming` for reference", + "The `stream` argument was set to `True` but the `stream_cls` argument was not given. See `writer._streaming` for reference", ) -class StreamAlreadyConsumed(WriterAIError): +class StreamAlreadyConsumed(WriterError): """ Attempted to read or stream content, but the content has already been streamed. diff --git a/src/writerai/_streaming.py b/src/writer/_streaming.py similarity index 99% rename from src/writerai/_streaming.py rename to src/writer/_streaming.py index a3c6f7d7..794ea612 100644 --- a/src/writerai/_streaming.py +++ b/src/writer/_streaming.py @@ -12,7 +12,7 @@ from ._utils import extract_type_var_from_base if TYPE_CHECKING: - from ._client import WriterAI, AsyncWriterAI + from ._client import Writer, AsyncWriter _T = TypeVar("_T") @@ -30,7 +30,7 @@ def __init__( *, cast_to: type[_T], response: httpx.Response, - client: WriterAI, + client: Writer, ) -> None: self.response = response self._cast_to = cast_to @@ -109,7 +109,7 @@ def __init__( *, cast_to: type[_T], response: httpx.Response, - client: AsyncWriterAI, + client: AsyncWriter, ) -> None: self.response = response self._cast_to = cast_to diff --git a/src/writerai/_types.py b/src/writer/_types.py similarity index 99% rename from src/writerai/_types.py rename to src/writer/_types.py index 7dbbaa54..3cbb7cf3 100644 --- a/src/writerai/_types.py +++ b/src/writer/_types.py @@ -81,7 +81,7 @@ # This unfortunately means that you will either have # to import this type and pass it explicitly: # -# from writerai import NoneType +# from writer import NoneType # client.get('/foo', cast_to=NoneType) # # or build it yourself: diff --git a/src/writerai/_utils/__init__.py b/src/writer/_utils/__init__.py similarity index 100% rename from src/writerai/_utils/__init__.py rename to src/writer/_utils/__init__.py diff --git a/src/writerai/_utils/_logs.py b/src/writer/_utils/_logs.py similarity index 71% rename from src/writerai/_utils/_logs.py rename to src/writer/_utils/_logs.py index 7dad39a6..fc983487 100644 --- a/src/writerai/_utils/_logs.py +++ b/src/writer/_utils/_logs.py @@ -1,12 +1,12 @@ import os import logging -logger: logging.Logger = logging.getLogger("writerai") +logger: logging.Logger = logging.getLogger("writer") httpx_logger: logging.Logger = logging.getLogger("httpx") def _basic_config() -> None: - # e.g. [2023-10-05 14:12:26 - writerai._base_client:818 - DEBUG] HTTP Request: POST http://127.0.0.1:4010/foo/bar "200 OK" + # e.g. [2023-10-05 14:12:26 - writer._base_client:818 - DEBUG] HTTP Request: POST http://127.0.0.1:4010/foo/bar "200 OK" logging.basicConfig( format="[%(asctime)s - %(name)s:%(lineno)d - %(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S", @@ -14,7 +14,7 @@ def _basic_config() -> None: def setup_logging() -> None: - env = os.environ.get("WRITERAI_LOG") + env = os.environ.get("WRITER_LOG") if env == "debug": _basic_config() logger.setLevel(logging.DEBUG) diff --git a/src/writerai/_utils/_proxy.py b/src/writer/_utils/_proxy.py similarity index 100% rename from src/writerai/_utils/_proxy.py rename to src/writer/_utils/_proxy.py diff --git a/src/writerai/_utils/_streams.py b/src/writer/_utils/_streams.py similarity index 100% rename from src/writerai/_utils/_streams.py rename to src/writer/_utils/_streams.py diff --git a/src/writerai/_utils/_sync.py b/src/writer/_utils/_sync.py similarity index 100% rename from src/writerai/_utils/_sync.py rename to src/writer/_utils/_sync.py diff --git a/src/writerai/_utils/_transform.py b/src/writer/_utils/_transform.py similarity index 100% rename from src/writerai/_utils/_transform.py rename to src/writer/_utils/_transform.py diff --git a/src/writerai/_utils/_typing.py b/src/writer/_utils/_typing.py similarity index 100% rename from src/writerai/_utils/_typing.py rename to src/writer/_utils/_typing.py diff --git a/src/writerai/_utils/_utils.py b/src/writer/_utils/_utils.py similarity index 100% rename from src/writerai/_utils/_utils.py rename to src/writer/_utils/_utils.py diff --git a/src/writerai/_version.py b/src/writer/_version.py similarity index 83% rename from src/writerai/_version.py rename to src/writer/_version.py index 4fca0ec6..3cd3f895 100644 --- a/src/writerai/_version.py +++ b/src/writer/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -__title__ = "writerai" +__title__ = "writer" __version__ = "0.0.1-alpha.0" diff --git a/src/writer/lib/.keep b/src/writer/lib/.keep new file mode 100644 index 00000000..5e2c99fd --- /dev/null +++ b/src/writer/lib/.keep @@ -0,0 +1,4 @@ +File generated from our OpenAPI spec by Stainless. + +This directory can be used to store custom files to expand the SDK. +It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. \ No newline at end of file diff --git a/src/writerai/py.typed b/src/writer/py.typed similarity index 100% rename from src/writerai/py.typed rename to src/writer/py.typed diff --git a/src/writerai/resources/__init__.py b/src/writer/resources/__init__.py similarity index 100% rename from src/writerai/resources/__init__.py rename to src/writer/resources/__init__.py diff --git a/src/writerai/resources/chat.py b/src/writer/resources/chat.py similarity index 100% rename from src/writerai/resources/chat.py rename to src/writer/resources/chat.py diff --git a/src/writerai/resources/completions.py b/src/writer/resources/completions.py similarity index 100% rename from src/writerai/resources/completions.py rename to src/writer/resources/completions.py diff --git a/src/writerai/resources/models.py b/src/writer/resources/models.py similarity index 100% rename from src/writerai/resources/models.py rename to src/writer/resources/models.py diff --git a/src/writerai/types/__init__.py b/src/writer/types/__init__.py similarity index 100% rename from src/writerai/types/__init__.py rename to src/writer/types/__init__.py diff --git a/src/writerai/types/chat.py b/src/writer/types/chat.py similarity index 100% rename from src/writerai/types/chat.py rename to src/writer/types/chat.py diff --git a/src/writerai/types/chat_chat_params.py b/src/writer/types/chat_chat_params.py similarity index 100% rename from src/writerai/types/chat_chat_params.py rename to src/writer/types/chat_chat_params.py diff --git a/src/writerai/types/chat_streaming_data.py b/src/writer/types/chat_streaming_data.py similarity index 100% rename from src/writerai/types/chat_streaming_data.py rename to src/writer/types/chat_streaming_data.py diff --git a/src/writerai/types/completion.py b/src/writer/types/completion.py similarity index 100% rename from src/writerai/types/completion.py rename to src/writer/types/completion.py diff --git a/src/writerai/types/completion_create_params.py b/src/writer/types/completion_create_params.py similarity index 100% rename from src/writerai/types/completion_create_params.py rename to src/writer/types/completion_create_params.py diff --git a/src/writerai/types/model_list_response.py b/src/writer/types/model_list_response.py similarity index 100% rename from src/writerai/types/model_list_response.py rename to src/writer/types/model_list_response.py diff --git a/src/writerai/types/streaming_data.py b/src/writer/types/streaming_data.py similarity index 100% rename from src/writerai/types/streaming_data.py rename to src/writer/types/streaming_data.py diff --git a/tests/api_resources/test_chat.py b/tests/api_resources/test_chat.py index 0c1b6863..9c97c1b3 100644 --- a/tests/api_resources/test_chat.py +++ b/tests/api_resources/test_chat.py @@ -7,9 +7,9 @@ import pytest -from writerai import WriterAI, AsyncWriterAI +from writer import Writer, AsyncWriter from tests.utils import assert_matches_type -from writerai.types import Chat +from writer.types import Chat base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -18,7 +18,7 @@ class TestChat: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) @parametrize - def test_method_chat_overload_1(self, client: WriterAI) -> None: + def test_method_chat_overload_1(self, client: Writer) -> None: chat = client.chat.chat( messages=[ { @@ -31,7 +31,7 @@ def test_method_chat_overload_1(self, client: WriterAI) -> None: assert_matches_type(Chat, chat, path=["response"]) @parametrize - def test_method_chat_with_all_params_overload_1(self, client: WriterAI) -> None: + def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: chat = client.chat.chat( messages=[ { @@ -51,7 +51,7 @@ def test_method_chat_with_all_params_overload_1(self, client: WriterAI) -> None: assert_matches_type(Chat, chat, path=["response"]) @parametrize - def test_raw_response_chat_overload_1(self, client: WriterAI) -> None: + def test_raw_response_chat_overload_1(self, client: Writer) -> None: response = client.chat.with_raw_response.chat( messages=[ { @@ -68,7 +68,7 @@ def test_raw_response_chat_overload_1(self, client: WriterAI) -> None: assert_matches_type(Chat, chat, path=["response"]) @parametrize - def test_streaming_response_chat_overload_1(self, client: WriterAI) -> None: + def test_streaming_response_chat_overload_1(self, client: Writer) -> None: with client.chat.with_streaming_response.chat( messages=[ { @@ -87,7 +87,7 @@ def test_streaming_response_chat_overload_1(self, client: WriterAI) -> None: assert cast(Any, response.is_closed) is True @parametrize - def test_method_chat_overload_2(self, client: WriterAI) -> None: + def test_method_chat_overload_2(self, client: Writer) -> None: chat_stream = client.chat.chat( messages=[ { @@ -101,7 +101,7 @@ def test_method_chat_overload_2(self, client: WriterAI) -> None: chat_stream.response.close() @parametrize - def test_method_chat_with_all_params_overload_2(self, client: WriterAI) -> None: + def test_method_chat_with_all_params_overload_2(self, client: Writer) -> None: chat_stream = client.chat.chat( messages=[ { @@ -121,7 +121,7 @@ def test_method_chat_with_all_params_overload_2(self, client: WriterAI) -> None: chat_stream.response.close() @parametrize - def test_raw_response_chat_overload_2(self, client: WriterAI) -> None: + def test_raw_response_chat_overload_2(self, client: Writer) -> None: response = client.chat.with_raw_response.chat( messages=[ { @@ -138,7 +138,7 @@ def test_raw_response_chat_overload_2(self, client: WriterAI) -> None: stream.close() @parametrize - def test_streaming_response_chat_overload_2(self, client: WriterAI) -> None: + def test_streaming_response_chat_overload_2(self, client: Writer) -> None: with client.chat.with_streaming_response.chat( messages=[ { @@ -162,7 +162,7 @@ class TestAsyncChat: parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) @parametrize - async def test_method_chat_overload_1(self, async_client: AsyncWriterAI) -> None: + async def test_method_chat_overload_1(self, async_client: AsyncWriter) -> None: chat = await async_client.chat.chat( messages=[ { @@ -175,7 +175,7 @@ async def test_method_chat_overload_1(self, async_client: AsyncWriterAI) -> None assert_matches_type(Chat, chat, path=["response"]) @parametrize - async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncWriterAI) -> None: + async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncWriter) -> None: chat = await async_client.chat.chat( messages=[ { @@ -195,7 +195,7 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW assert_matches_type(Chat, chat, path=["response"]) @parametrize - async def test_raw_response_chat_overload_1(self, async_client: AsyncWriterAI) -> None: + async def test_raw_response_chat_overload_1(self, async_client: AsyncWriter) -> None: response = await async_client.chat.with_raw_response.chat( messages=[ { @@ -212,7 +212,7 @@ async def test_raw_response_chat_overload_1(self, async_client: AsyncWriterAI) - assert_matches_type(Chat, chat, path=["response"]) @parametrize - async def test_streaming_response_chat_overload_1(self, async_client: AsyncWriterAI) -> None: + async def test_streaming_response_chat_overload_1(self, async_client: AsyncWriter) -> None: async with async_client.chat.with_streaming_response.chat( messages=[ { @@ -231,7 +231,7 @@ async def test_streaming_response_chat_overload_1(self, async_client: AsyncWrite assert cast(Any, response.is_closed) is True @parametrize - async def test_method_chat_overload_2(self, async_client: AsyncWriterAI) -> None: + async def test_method_chat_overload_2(self, async_client: AsyncWriter) -> None: chat_stream = await async_client.chat.chat( messages=[ { @@ -245,7 +245,7 @@ async def test_method_chat_overload_2(self, async_client: AsyncWriterAI) -> None await chat_stream.response.aclose() @parametrize - async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncWriterAI) -> None: + async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncWriter) -> None: chat_stream = await async_client.chat.chat( messages=[ { @@ -265,7 +265,7 @@ async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncW await chat_stream.response.aclose() @parametrize - async def test_raw_response_chat_overload_2(self, async_client: AsyncWriterAI) -> None: + async def test_raw_response_chat_overload_2(self, async_client: AsyncWriter) -> None: response = await async_client.chat.with_raw_response.chat( messages=[ { @@ -282,7 +282,7 @@ async def test_raw_response_chat_overload_2(self, async_client: AsyncWriterAI) - await stream.close() @parametrize - async def test_streaming_response_chat_overload_2(self, async_client: AsyncWriterAI) -> None: + async def test_streaming_response_chat_overload_2(self, async_client: AsyncWriter) -> None: async with async_client.chat.with_streaming_response.chat( messages=[ { diff --git a/tests/api_resources/test_completions.py b/tests/api_resources/test_completions.py index 70172d6f..28086f47 100644 --- a/tests/api_resources/test_completions.py +++ b/tests/api_resources/test_completions.py @@ -7,9 +7,9 @@ import pytest -from writerai import WriterAI, AsyncWriterAI +from writer import Writer, AsyncWriter from tests.utils import assert_matches_type -from writerai.types import Completion +from writer.types import Completion base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -18,7 +18,7 @@ class TestCompletions: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) @parametrize - def test_method_create_overload_1(self, client: WriterAI) -> None: + def test_method_create_overload_1(self, client: Writer) -> None: completion = client.completions.create( model="string", prompt="string", @@ -26,7 +26,7 @@ def test_method_create_overload_1(self, client: WriterAI) -> None: assert_matches_type(Completion, completion, path=["response"]) @parametrize - def test_method_create_with_all_params_overload_1(self, client: WriterAI) -> None: + def test_method_create_with_all_params_overload_1(self, client: Writer) -> None: completion = client.completions.create( model="string", prompt="string", @@ -41,7 +41,7 @@ def test_method_create_with_all_params_overload_1(self, client: WriterAI) -> Non assert_matches_type(Completion, completion, path=["response"]) @parametrize - def test_raw_response_create_overload_1(self, client: WriterAI) -> None: + def test_raw_response_create_overload_1(self, client: Writer) -> None: response = client.completions.with_raw_response.create( model="string", prompt="string", @@ -53,7 +53,7 @@ def test_raw_response_create_overload_1(self, client: WriterAI) -> None: assert_matches_type(Completion, completion, path=["response"]) @parametrize - def test_streaming_response_create_overload_1(self, client: WriterAI) -> None: + def test_streaming_response_create_overload_1(self, client: Writer) -> None: with client.completions.with_streaming_response.create( model="string", prompt="string", @@ -67,7 +67,7 @@ def test_streaming_response_create_overload_1(self, client: WriterAI) -> None: assert cast(Any, response.is_closed) is True @parametrize - def test_method_create_overload_2(self, client: WriterAI) -> None: + def test_method_create_overload_2(self, client: Writer) -> None: completion_stream = client.completions.create( model="string", prompt="string", @@ -76,7 +76,7 @@ def test_method_create_overload_2(self, client: WriterAI) -> None: completion_stream.response.close() @parametrize - def test_method_create_with_all_params_overload_2(self, client: WriterAI) -> None: + def test_method_create_with_all_params_overload_2(self, client: Writer) -> None: completion_stream = client.completions.create( model="string", prompt="string", @@ -91,7 +91,7 @@ def test_method_create_with_all_params_overload_2(self, client: WriterAI) -> Non completion_stream.response.close() @parametrize - def test_raw_response_create_overload_2(self, client: WriterAI) -> None: + def test_raw_response_create_overload_2(self, client: Writer) -> None: response = client.completions.with_raw_response.create( model="string", prompt="string", @@ -103,7 +103,7 @@ def test_raw_response_create_overload_2(self, client: WriterAI) -> None: stream.close() @parametrize - def test_streaming_response_create_overload_2(self, client: WriterAI) -> None: + def test_streaming_response_create_overload_2(self, client: Writer) -> None: with client.completions.with_streaming_response.create( model="string", prompt="string", @@ -122,7 +122,7 @@ class TestAsyncCompletions: parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) @parametrize - async def test_method_create_overload_1(self, async_client: AsyncWriterAI) -> None: + async def test_method_create_overload_1(self, async_client: AsyncWriter) -> None: completion = await async_client.completions.create( model="string", prompt="string", @@ -130,7 +130,7 @@ async def test_method_create_overload_1(self, async_client: AsyncWriterAI) -> No assert_matches_type(Completion, completion, path=["response"]) @parametrize - async def test_method_create_with_all_params_overload_1(self, async_client: AsyncWriterAI) -> None: + async def test_method_create_with_all_params_overload_1(self, async_client: AsyncWriter) -> None: completion = await async_client.completions.create( model="string", prompt="string", @@ -145,7 +145,7 @@ async def test_method_create_with_all_params_overload_1(self, async_client: Asyn assert_matches_type(Completion, completion, path=["response"]) @parametrize - async def test_raw_response_create_overload_1(self, async_client: AsyncWriterAI) -> None: + async def test_raw_response_create_overload_1(self, async_client: AsyncWriter) -> None: response = await async_client.completions.with_raw_response.create( model="string", prompt="string", @@ -157,7 +157,7 @@ async def test_raw_response_create_overload_1(self, async_client: AsyncWriterAI) assert_matches_type(Completion, completion, path=["response"]) @parametrize - async def test_streaming_response_create_overload_1(self, async_client: AsyncWriterAI) -> None: + async def test_streaming_response_create_overload_1(self, async_client: AsyncWriter) -> None: async with async_client.completions.with_streaming_response.create( model="string", prompt="string", @@ -171,7 +171,7 @@ async def test_streaming_response_create_overload_1(self, async_client: AsyncWri assert cast(Any, response.is_closed) is True @parametrize - async def test_method_create_overload_2(self, async_client: AsyncWriterAI) -> None: + async def test_method_create_overload_2(self, async_client: AsyncWriter) -> None: completion_stream = await async_client.completions.create( model="string", prompt="string", @@ -180,7 +180,7 @@ async def test_method_create_overload_2(self, async_client: AsyncWriterAI) -> No await completion_stream.response.aclose() @parametrize - async def test_method_create_with_all_params_overload_2(self, async_client: AsyncWriterAI) -> None: + async def test_method_create_with_all_params_overload_2(self, async_client: AsyncWriter) -> None: completion_stream = await async_client.completions.create( model="string", prompt="string", @@ -195,7 +195,7 @@ async def test_method_create_with_all_params_overload_2(self, async_client: Asyn await completion_stream.response.aclose() @parametrize - async def test_raw_response_create_overload_2(self, async_client: AsyncWriterAI) -> None: + async def test_raw_response_create_overload_2(self, async_client: AsyncWriter) -> None: response = await async_client.completions.with_raw_response.create( model="string", prompt="string", @@ -207,7 +207,7 @@ async def test_raw_response_create_overload_2(self, async_client: AsyncWriterAI) await stream.close() @parametrize - async def test_streaming_response_create_overload_2(self, async_client: AsyncWriterAI) -> None: + async def test_streaming_response_create_overload_2(self, async_client: AsyncWriter) -> None: async with async_client.completions.with_streaming_response.create( model="string", prompt="string", diff --git a/tests/api_resources/test_models.py b/tests/api_resources/test_models.py index 083fd6ce..e7d78ff2 100644 --- a/tests/api_resources/test_models.py +++ b/tests/api_resources/test_models.py @@ -7,9 +7,9 @@ import pytest -from writerai import WriterAI, AsyncWriterAI +from writer import Writer, AsyncWriter from tests.utils import assert_matches_type -from writerai.types import ModelListResponse +from writer.types import ModelListResponse base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -18,12 +18,12 @@ class TestModels: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) @parametrize - def test_method_list(self, client: WriterAI) -> None: + def test_method_list(self, client: Writer) -> None: model = client.models.list() assert_matches_type(ModelListResponse, model, path=["response"]) @parametrize - def test_raw_response_list(self, client: WriterAI) -> None: + def test_raw_response_list(self, client: Writer) -> None: response = client.models.with_raw_response.list() assert response.is_closed is True @@ -32,7 +32,7 @@ def test_raw_response_list(self, client: WriterAI) -> None: assert_matches_type(ModelListResponse, model, path=["response"]) @parametrize - def test_streaming_response_list(self, client: WriterAI) -> None: + def test_streaming_response_list(self, client: Writer) -> None: with client.models.with_streaming_response.list() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -47,12 +47,12 @@ class TestAsyncModels: parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) @parametrize - async def test_method_list(self, async_client: AsyncWriterAI) -> None: + async def test_method_list(self, async_client: AsyncWriter) -> None: model = await async_client.models.list() assert_matches_type(ModelListResponse, model, path=["response"]) @parametrize - async def test_raw_response_list(self, async_client: AsyncWriterAI) -> None: + async def test_raw_response_list(self, async_client: AsyncWriter) -> None: response = await async_client.models.with_raw_response.list() assert response.is_closed is True @@ -61,7 +61,7 @@ async def test_raw_response_list(self, async_client: AsyncWriterAI) -> None: assert_matches_type(ModelListResponse, model, path=["response"]) @parametrize - async def test_streaming_response_list(self, async_client: AsyncWriterAI) -> None: + async def test_streaming_response_list(self, async_client: AsyncWriter) -> None: async with async_client.models.with_streaming_response.list() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" diff --git a/tests/conftest.py b/tests/conftest.py index 91b8fbf0..26f0840f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,14 +7,14 @@ import pytest -from writerai import WriterAI, AsyncWriterAI +from writer import Writer, AsyncWriter if TYPE_CHECKING: from _pytest.fixtures import FixtureRequest pytest.register_assert_rewrite("tests.utils") -logging.getLogger("writerai").setLevel(logging.DEBUG) +logging.getLogger("writer").setLevel(logging.DEBUG) @pytest.fixture(scope="session") @@ -30,20 +30,20 @@ def event_loop() -> Iterator[asyncio.AbstractEventLoop]: @pytest.fixture(scope="session") -def client(request: FixtureRequest) -> Iterator[WriterAI]: +def client(request: FixtureRequest) -> Iterator[Writer]: strict = getattr(request, "param", True) if not isinstance(strict, bool): raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") - with WriterAI(base_url=base_url, api_key=api_key, _strict_response_validation=strict) as client: + with Writer(base_url=base_url, api_key=api_key, _strict_response_validation=strict) as client: yield client @pytest.fixture(scope="session") -async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncWriterAI]: +async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncWriter]: strict = getattr(request, "param", True) if not isinstance(strict, bool): raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") - async with AsyncWriterAI(base_url=base_url, api_key=api_key, _strict_response_validation=strict) as client: + async with AsyncWriter(base_url=base_url, api_key=api_key, _strict_response_validation=strict) as client: yield client diff --git a/tests/test_client.py b/tests/test_client.py index c9f387ff..b1cfd8f8 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -16,17 +16,12 @@ from respx import MockRouter from pydantic import ValidationError -from writerai import WriterAI, AsyncWriterAI, APIResponseValidationError -from writerai._models import BaseModel, FinalRequestOptions -from writerai._constants import RAW_RESPONSE_HEADER -from writerai._streaming import Stream, AsyncStream -from writerai._exceptions import WriterAIError, APIStatusError, APITimeoutError, APIResponseValidationError -from writerai._base_client import ( - DEFAULT_TIMEOUT, - HTTPX_DEFAULT_TIMEOUT, - BaseClient, - make_request_options, -) +from writer import Writer, AsyncWriter, APIResponseValidationError +from writer._models import BaseModel, FinalRequestOptions +from writer._constants import RAW_RESPONSE_HEADER +from writer._streaming import Stream, AsyncStream +from writer._exceptions import WriterError, APIStatusError, APITimeoutError, APIResponseValidationError +from writer._base_client import DEFAULT_TIMEOUT, HTTPX_DEFAULT_TIMEOUT, BaseClient, make_request_options from .utils import update_env @@ -44,7 +39,7 @@ def _low_retry_timeout(*_args: Any, **_kwargs: Any) -> float: return 0.1 -def _get_open_connections(client: WriterAI | AsyncWriterAI) -> int: +def _get_open_connections(client: Writer | AsyncWriter) -> int: transport = client._client._transport assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport) @@ -52,8 +47,8 @@ def _get_open_connections(client: WriterAI | AsyncWriterAI) -> int: return len(pool._requests) -class TestWriterAI: - client = WriterAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) +class TestWriter: + client = Writer(base_url=base_url, api_key=api_key, _strict_response_validation=True) @pytest.mark.respx(base_url=base_url) def test_raw_response(self, respx_mock: MockRouter) -> None: @@ -100,7 +95,7 @@ def test_copy_default_options(self) -> None: assert isinstance(self.client.timeout, httpx.Timeout) def test_copy_default_headers(self) -> None: - client = WriterAI( + client = Writer( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) assert client.default_headers["X-Foo"] == "bar" @@ -134,7 +129,7 @@ def test_copy_default_headers(self) -> None: client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"}) def test_copy_default_query(self) -> None: - client = WriterAI( + client = Writer( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"} ) assert _get_params(client)["foo"] == "bar" @@ -225,10 +220,10 @@ def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.Statistic # to_raw_response_wrapper leaks through the @functools.wraps() decorator. # # removing the decorator fixes the leak for reasons we don't understand. - "writerai/_legacy_response.py", - "writerai/_response.py", + "writer/_legacy_response.py", + "writer/_response.py", # pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason. - "writerai/_compat.py", + "writer/_compat.py", # Standard library leaks we don't care about. "/logging/__init__.py", ] @@ -259,9 +254,7 @@ def test_request_timeout(self) -> None: assert timeout == httpx.Timeout(100.0) def test_client_timeout_option(self) -> None: - client = WriterAI( - base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0) - ) + client = Writer(base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0)) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore @@ -270,7 +263,7 @@ def test_client_timeout_option(self) -> None: def test_http_client_timeout_option(self) -> None: # custom timeout given to the httpx client should be used with httpx.Client(timeout=None) as http_client: - client = WriterAI( + client = Writer( base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client ) @@ -280,7 +273,7 @@ def test_http_client_timeout_option(self) -> None: # no timeout given to the httpx client should not use the httpx default with httpx.Client() as http_client: - client = WriterAI( + client = Writer( base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client ) @@ -290,7 +283,7 @@ def test_http_client_timeout_option(self) -> None: # explicitly passing the default timeout currently results in it being ignored with httpx.Client(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: - client = WriterAI( + client = Writer( base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client ) @@ -301,7 +294,7 @@ def test_http_client_timeout_option(self) -> None: async def test_invalid_http_client(self) -> None: with pytest.raises(TypeError, match="Invalid `http_client` arg"): async with httpx.AsyncClient() as http_client: - WriterAI( + Writer( base_url=base_url, api_key=api_key, _strict_response_validation=True, @@ -309,14 +302,14 @@ async def test_invalid_http_client(self) -> None: ) def test_default_headers_option(self) -> None: - client = WriterAI( + client = Writer( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "bar" assert request.headers.get("x-stainless-lang") == "python" - client2 = WriterAI( + client2 = Writer( base_url=base_url, api_key=api_key, _strict_response_validation=True, @@ -330,16 +323,16 @@ def test_default_headers_option(self) -> None: assert request.headers.get("x-stainless-lang") == "my-overriding-header" def test_validate_headers(self) -> None: - client = WriterAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) + client = Writer(base_url=base_url, api_key=api_key, _strict_response_validation=True) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("Authorization") == f"Bearer {api_key}" - with pytest.raises(WriterAIError): - client2 = WriterAI(base_url=base_url, api_key=None, _strict_response_validation=True) + with pytest.raises(WriterError): + client2 = Writer(base_url=base_url, api_key=None, _strict_response_validation=True) _ = client2 def test_default_query_option(self) -> None: - client = WriterAI( + client = Writer( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -453,7 +446,7 @@ def test_request_extra_query(self) -> None: params = dict(request.url.params) assert params == {"foo": "2"} - def test_multipart_repeating_array(self, client: WriterAI) -> None: + def test_multipart_repeating_array(self, client: Writer) -> None: request = client._build_request( FinalRequestOptions.construct( method="get", @@ -540,7 +533,7 @@ class Model(BaseModel): assert response.foo == 2 def test_base_url_setter(self) -> None: - client = WriterAI(base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True) + client = Writer(base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True) assert client.base_url == "https://example.com/from_init/" client.base_url = "https://example.com/from_setter" # type: ignore[assignment] @@ -548,15 +541,15 @@ def test_base_url_setter(self) -> None: assert client.base_url == "https://example.com/from_setter/" def test_base_url_env(self) -> None: - with update_env(WRITERAI_BASE_URL="http://localhost:5000/from/env"): - client = WriterAI(api_key=api_key, _strict_response_validation=True) + with update_env(WRITER_BASE_URL="http://localhost:5000/from/env"): + client = Writer(api_key=api_key, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" @pytest.mark.parametrize( "client", [ - WriterAI(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), - WriterAI( + Writer(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), + Writer( base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True, @@ -565,7 +558,7 @@ def test_base_url_env(self) -> None: ], ids=["standard", "custom http client"], ) - def test_base_url_trailing_slash(self, client: WriterAI) -> None: + def test_base_url_trailing_slash(self, client: Writer) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -578,8 +571,8 @@ def test_base_url_trailing_slash(self, client: WriterAI) -> None: @pytest.mark.parametrize( "client", [ - WriterAI(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), - WriterAI( + Writer(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), + Writer( base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True, @@ -588,7 +581,7 @@ def test_base_url_trailing_slash(self, client: WriterAI) -> None: ], ids=["standard", "custom http client"], ) - def test_base_url_no_trailing_slash(self, client: WriterAI) -> None: + def test_base_url_no_trailing_slash(self, client: Writer) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -601,8 +594,8 @@ def test_base_url_no_trailing_slash(self, client: WriterAI) -> None: @pytest.mark.parametrize( "client", [ - WriterAI(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), - WriterAI( + Writer(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), + Writer( base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True, @@ -611,7 +604,7 @@ def test_base_url_no_trailing_slash(self, client: WriterAI) -> None: ], ids=["standard", "custom http client"], ) - def test_absolute_request_url(self, client: WriterAI) -> None: + def test_absolute_request_url(self, client: Writer) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -622,7 +615,7 @@ def test_absolute_request_url(self, client: WriterAI) -> None: assert request.url == "https://myapi.com/foo" def test_copied_client_does_not_close_http(self) -> None: - client = WriterAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) + client = Writer(base_url=base_url, api_key=api_key, _strict_response_validation=True) assert not client.is_closed() copied = client.copy() @@ -633,7 +626,7 @@ def test_copied_client_does_not_close_http(self) -> None: assert not client.is_closed() def test_client_context_manager(self) -> None: - client = WriterAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) + client = Writer(base_url=base_url, api_key=api_key, _strict_response_validation=True) with client as c2: assert c2 is client assert not c2.is_closed() @@ -654,7 +647,7 @@ class Model(BaseModel): def test_client_max_retries_validation(self) -> None: with pytest.raises(TypeError, match=r"max_retries cannot be None"): - WriterAI(base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None)) + Writer(base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None)) @pytest.mark.respx(base_url=base_url) def test_default_stream_cls(self, respx_mock: MockRouter) -> None: @@ -674,12 +667,12 @@ class Model(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) - strict_client = WriterAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) + strict_client = Writer(base_url=base_url, api_key=api_key, _strict_response_validation=True) with pytest.raises(APIResponseValidationError): strict_client.get("/foo", cast_to=Model) - client = WriterAI(base_url=base_url, api_key=api_key, _strict_response_validation=False) + client = Writer(base_url=base_url, api_key=api_key, _strict_response_validation=False) response = client.get("/foo", cast_to=Model) assert isinstance(response, str) # type: ignore[unreachable] @@ -706,14 +699,14 @@ class Model(BaseModel): ) @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None: - client = WriterAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) + client = Writer(base_url=base_url, api_key=api_key, _strict_response_validation=True) headers = httpx.Headers({"retry-after": retry_after}) options = FinalRequestOptions(method="get", url="/foo", max_retries=3) calculated = client._calculate_retry_timeout(remaining_retries, options, headers) assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType] - @mock.patch("writerai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @mock.patch("writer._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: respx_mock.post("/v1/chat").mock(side_effect=httpx.TimeoutException("Test timeout error")) @@ -739,7 +732,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> No assert _get_open_connections(self.client) == 0 - @mock.patch("writerai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @mock.patch("writer._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: respx_mock.post("/v1/chat").mock(return_value=httpx.Response(500)) @@ -766,8 +759,8 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> Non assert _get_open_connections(self.client) == 0 -class TestAsyncWriterAI: - client = AsyncWriterAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) +class TestAsyncWriter: + client = AsyncWriter(base_url=base_url, api_key=api_key, _strict_response_validation=True) @pytest.mark.respx(base_url=base_url) @pytest.mark.asyncio @@ -816,7 +809,7 @@ def test_copy_default_options(self) -> None: assert isinstance(self.client.timeout, httpx.Timeout) def test_copy_default_headers(self) -> None: - client = AsyncWriterAI( + client = AsyncWriter( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) assert client.default_headers["X-Foo"] == "bar" @@ -850,7 +843,7 @@ def test_copy_default_headers(self) -> None: client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"}) def test_copy_default_query(self) -> None: - client = AsyncWriterAI( + client = AsyncWriter( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"} ) assert _get_params(client)["foo"] == "bar" @@ -941,10 +934,10 @@ def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.Statistic # to_raw_response_wrapper leaks through the @functools.wraps() decorator. # # removing the decorator fixes the leak for reasons we don't understand. - "writerai/_legacy_response.py", - "writerai/_response.py", + "writer/_legacy_response.py", + "writer/_response.py", # pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason. - "writerai/_compat.py", + "writer/_compat.py", # Standard library leaks we don't care about. "/logging/__init__.py", ] @@ -975,7 +968,7 @@ async def test_request_timeout(self) -> None: assert timeout == httpx.Timeout(100.0) async def test_client_timeout_option(self) -> None: - client = AsyncWriterAI( + client = AsyncWriter( base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0) ) @@ -986,7 +979,7 @@ async def test_client_timeout_option(self) -> None: async def test_http_client_timeout_option(self) -> None: # custom timeout given to the httpx client should be used async with httpx.AsyncClient(timeout=None) as http_client: - client = AsyncWriterAI( + client = AsyncWriter( base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client ) @@ -996,7 +989,7 @@ async def test_http_client_timeout_option(self) -> None: # no timeout given to the httpx client should not use the httpx default async with httpx.AsyncClient() as http_client: - client = AsyncWriterAI( + client = AsyncWriter( base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client ) @@ -1006,7 +999,7 @@ async def test_http_client_timeout_option(self) -> None: # explicitly passing the default timeout currently results in it being ignored async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: - client = AsyncWriterAI( + client = AsyncWriter( base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client ) @@ -1017,7 +1010,7 @@ async def test_http_client_timeout_option(self) -> None: def test_invalid_http_client(self) -> None: with pytest.raises(TypeError, match="Invalid `http_client` arg"): with httpx.Client() as http_client: - AsyncWriterAI( + AsyncWriter( base_url=base_url, api_key=api_key, _strict_response_validation=True, @@ -1025,14 +1018,14 @@ def test_invalid_http_client(self) -> None: ) def test_default_headers_option(self) -> None: - client = AsyncWriterAI( + client = AsyncWriter( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "bar" assert request.headers.get("x-stainless-lang") == "python" - client2 = AsyncWriterAI( + client2 = AsyncWriter( base_url=base_url, api_key=api_key, _strict_response_validation=True, @@ -1046,16 +1039,16 @@ def test_default_headers_option(self) -> None: assert request.headers.get("x-stainless-lang") == "my-overriding-header" def test_validate_headers(self) -> None: - client = AsyncWriterAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) + client = AsyncWriter(base_url=base_url, api_key=api_key, _strict_response_validation=True) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("Authorization") == f"Bearer {api_key}" - with pytest.raises(WriterAIError): - client2 = AsyncWriterAI(base_url=base_url, api_key=None, _strict_response_validation=True) + with pytest.raises(WriterError): + client2 = AsyncWriter(base_url=base_url, api_key=None, _strict_response_validation=True) _ = client2 def test_default_query_option(self) -> None: - client = AsyncWriterAI( + client = AsyncWriter( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -1169,7 +1162,7 @@ def test_request_extra_query(self) -> None: params = dict(request.url.params) assert params == {"foo": "2"} - def test_multipart_repeating_array(self, async_client: AsyncWriterAI) -> None: + def test_multipart_repeating_array(self, async_client: AsyncWriter) -> None: request = async_client._build_request( FinalRequestOptions.construct( method="get", @@ -1256,7 +1249,7 @@ class Model(BaseModel): assert response.foo == 2 def test_base_url_setter(self) -> None: - client = AsyncWriterAI( + client = AsyncWriter( base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True ) assert client.base_url == "https://example.com/from_init/" @@ -1266,17 +1259,17 @@ def test_base_url_setter(self) -> None: assert client.base_url == "https://example.com/from_setter/" def test_base_url_env(self) -> None: - with update_env(WRITERAI_BASE_URL="http://localhost:5000/from/env"): - client = AsyncWriterAI(api_key=api_key, _strict_response_validation=True) + with update_env(WRITER_BASE_URL="http://localhost:5000/from/env"): + client = AsyncWriter(api_key=api_key, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" @pytest.mark.parametrize( "client", [ - AsyncWriterAI( + AsyncWriter( base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True ), - AsyncWriterAI( + AsyncWriter( base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True, @@ -1285,7 +1278,7 @@ def test_base_url_env(self) -> None: ], ids=["standard", "custom http client"], ) - def test_base_url_trailing_slash(self, client: AsyncWriterAI) -> None: + def test_base_url_trailing_slash(self, client: AsyncWriter) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -1298,10 +1291,10 @@ def test_base_url_trailing_slash(self, client: AsyncWriterAI) -> None: @pytest.mark.parametrize( "client", [ - AsyncWriterAI( + AsyncWriter( base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True ), - AsyncWriterAI( + AsyncWriter( base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True, @@ -1310,7 +1303,7 @@ def test_base_url_trailing_slash(self, client: AsyncWriterAI) -> None: ], ids=["standard", "custom http client"], ) - def test_base_url_no_trailing_slash(self, client: AsyncWriterAI) -> None: + def test_base_url_no_trailing_slash(self, client: AsyncWriter) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -1323,10 +1316,10 @@ def test_base_url_no_trailing_slash(self, client: AsyncWriterAI) -> None: @pytest.mark.parametrize( "client", [ - AsyncWriterAI( + AsyncWriter( base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True ), - AsyncWriterAI( + AsyncWriter( base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True, @@ -1335,7 +1328,7 @@ def test_base_url_no_trailing_slash(self, client: AsyncWriterAI) -> None: ], ids=["standard", "custom http client"], ) - def test_absolute_request_url(self, client: AsyncWriterAI) -> None: + def test_absolute_request_url(self, client: AsyncWriter) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -1346,7 +1339,7 @@ def test_absolute_request_url(self, client: AsyncWriterAI) -> None: assert request.url == "https://myapi.com/foo" async def test_copied_client_does_not_close_http(self) -> None: - client = AsyncWriterAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) + client = AsyncWriter(base_url=base_url, api_key=api_key, _strict_response_validation=True) assert not client.is_closed() copied = client.copy() @@ -1358,7 +1351,7 @@ async def test_copied_client_does_not_close_http(self) -> None: assert not client.is_closed() async def test_client_context_manager(self) -> None: - client = AsyncWriterAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) + client = AsyncWriter(base_url=base_url, api_key=api_key, _strict_response_validation=True) async with client as c2: assert c2 is client assert not c2.is_closed() @@ -1380,7 +1373,7 @@ class Model(BaseModel): async def test_client_max_retries_validation(self) -> None: with pytest.raises(TypeError, match=r"max_retries cannot be None"): - AsyncWriterAI( + AsyncWriter( base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None) ) @@ -1404,12 +1397,12 @@ class Model(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) - strict_client = AsyncWriterAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) + strict_client = AsyncWriter(base_url=base_url, api_key=api_key, _strict_response_validation=True) with pytest.raises(APIResponseValidationError): await strict_client.get("/foo", cast_to=Model) - client = AsyncWriterAI(base_url=base_url, api_key=api_key, _strict_response_validation=False) + client = AsyncWriter(base_url=base_url, api_key=api_key, _strict_response_validation=False) response = await client.get("/foo", cast_to=Model) assert isinstance(response, str) # type: ignore[unreachable] @@ -1437,14 +1430,14 @@ class Model(BaseModel): @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) @pytest.mark.asyncio async def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None: - client = AsyncWriterAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) + client = AsyncWriter(base_url=base_url, api_key=api_key, _strict_response_validation=True) headers = httpx.Headers({"retry-after": retry_after}) options = FinalRequestOptions(method="get", url="/foo", max_retries=3) calculated = client._calculate_retry_timeout(remaining_retries, options, headers) assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType] - @mock.patch("writerai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @mock.patch("writer._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: respx_mock.post("/v1/chat").mock(side_effect=httpx.TimeoutException("Test timeout error")) @@ -1470,7 +1463,7 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) assert _get_open_connections(self.client) == 0 - @mock.patch("writerai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @mock.patch("writer._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: respx_mock.post("/v1/chat").mock(return_value=httpx.Response(500)) diff --git a/tests/test_deepcopy.py b/tests/test_deepcopy.py index 628e4049..71f6fa67 100644 --- a/tests/test_deepcopy.py +++ b/tests/test_deepcopy.py @@ -1,4 +1,4 @@ -from writerai._utils import deepcopy_minimal +from writer._utils import deepcopy_minimal def assert_different_identities(obj1: object, obj2: object) -> None: diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py index 9d9a4b19..7c1bf12f 100644 --- a/tests/test_extract_files.py +++ b/tests/test_extract_files.py @@ -4,8 +4,8 @@ import pytest -from writerai._types import FileTypes -from writerai._utils import extract_files +from writer._types import FileTypes +from writer._utils import extract_files def test_removes_files_from_input() -> None: diff --git a/tests/test_files.py b/tests/test_files.py index 9cc66fe2..1df82803 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -4,7 +4,7 @@ import pytest from dirty_equals import IsDict, IsList, IsBytes, IsTuple -from writerai._files import to_httpx_files, async_to_httpx_files +from writer._files import to_httpx_files, async_to_httpx_files readme_path = Path(__file__).parent.parent.joinpath("README.md") diff --git a/tests/test_models.py b/tests/test_models.py index fa64230d..080df657 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -7,9 +7,9 @@ import pydantic from pydantic import Field -from writerai._utils import PropertyInfo -from writerai._compat import PYDANTIC_V2, parse_obj, model_dump, model_json -from writerai._models import BaseModel, construct_type +from writer._utils import PropertyInfo +from writer._compat import PYDANTIC_V2, parse_obj, model_dump, model_json +from writer._models import BaseModel, construct_type class BasicModel(BaseModel): diff --git a/tests/test_qs.py b/tests/test_qs.py index 3dbda131..f49cc1b9 100644 --- a/tests/test_qs.py +++ b/tests/test_qs.py @@ -4,7 +4,7 @@ import pytest -from writerai._qs import Querystring, stringify +from writer._qs import Querystring, stringify def test_empty() -> None: diff --git a/tests/test_required_args.py b/tests/test_required_args.py index db5f699c..d2165d45 100644 --- a/tests/test_required_args.py +++ b/tests/test_required_args.py @@ -2,7 +2,7 @@ import pytest -from writerai._utils import required_args +from writer._utils import required_args def test_too_many_positional_params() -> None: diff --git a/tests/test_response.py b/tests/test_response.py index 769d7f7f..db65163a 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -6,8 +6,8 @@ import pytest import pydantic -from writerai import WriterAI, BaseModel, AsyncWriterAI -from writerai._response import ( +from writer import Writer, BaseModel, AsyncWriter +from writer._response import ( APIResponse, BaseAPIResponse, AsyncAPIResponse, @@ -15,8 +15,8 @@ AsyncBinaryAPIResponse, extract_response_type, ) -from writerai._streaming import Stream -from writerai._base_client import FinalRequestOptions +from writer._streaming import Stream +from writer._base_client import FinalRequestOptions class ConcreteBaseAPIResponse(APIResponse[bytes]): @@ -40,7 +40,7 @@ def test_extract_response_type_direct_classes() -> None: def test_extract_response_type_direct_class_missing_type_arg() -> None: with pytest.raises( RuntimeError, - match="Expected type to have a type argument at index 0 but it did not", + match="Expected type to have a type argument at index 0 but it did not", ): extract_response_type(AsyncAPIResponse) @@ -60,7 +60,7 @@ class PydanticModel(pydantic.BaseModel): ... -def test_response_parse_mismatched_basemodel(client: WriterAI) -> None: +def test_response_parse_mismatched_basemodel(client: Writer) -> None: response = APIResponse( raw=httpx.Response(200, content=b"foo"), client=client, @@ -72,13 +72,13 @@ def test_response_parse_mismatched_basemodel(client: WriterAI) -> None: with pytest.raises( TypeError, - match="Pydantic models must subclass our base model type, e.g. `from writerai import BaseModel`", + match="Pydantic models must subclass our base model type, e.g. `from writer import BaseModel`", ): response.parse(to=PydanticModel) @pytest.mark.asyncio -async def test_async_response_parse_mismatched_basemodel(async_client: AsyncWriterAI) -> None: +async def test_async_response_parse_mismatched_basemodel(async_client: AsyncWriter) -> None: response = AsyncAPIResponse( raw=httpx.Response(200, content=b"foo"), client=async_client, @@ -90,12 +90,12 @@ async def test_async_response_parse_mismatched_basemodel(async_client: AsyncWrit with pytest.raises( TypeError, - match="Pydantic models must subclass our base model type, e.g. `from writerai import BaseModel`", + match="Pydantic models must subclass our base model type, e.g. `from writer import BaseModel`", ): await response.parse(to=PydanticModel) -def test_response_parse_custom_stream(client: WriterAI) -> None: +def test_response_parse_custom_stream(client: Writer) -> None: response = APIResponse( raw=httpx.Response(200, content=b"foo"), client=client, @@ -110,7 +110,7 @@ def test_response_parse_custom_stream(client: WriterAI) -> None: @pytest.mark.asyncio -async def test_async_response_parse_custom_stream(async_client: AsyncWriterAI) -> None: +async def test_async_response_parse_custom_stream(async_client: AsyncWriter) -> None: response = AsyncAPIResponse( raw=httpx.Response(200, content=b"foo"), client=async_client, @@ -129,7 +129,7 @@ class CustomModel(BaseModel): bar: int -def test_response_parse_custom_model(client: WriterAI) -> None: +def test_response_parse_custom_model(client: Writer) -> None: response = APIResponse( raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), client=client, @@ -145,7 +145,7 @@ def test_response_parse_custom_model(client: WriterAI) -> None: @pytest.mark.asyncio -async def test_async_response_parse_custom_model(async_client: AsyncWriterAI) -> None: +async def test_async_response_parse_custom_model(async_client: AsyncWriter) -> None: response = AsyncAPIResponse( raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), client=async_client, @@ -160,7 +160,7 @@ async def test_async_response_parse_custom_model(async_client: AsyncWriterAI) -> assert obj.bar == 2 -def test_response_parse_annotated_type(client: WriterAI) -> None: +def test_response_parse_annotated_type(client: Writer) -> None: response = APIResponse( raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), client=client, @@ -177,7 +177,7 @@ def test_response_parse_annotated_type(client: WriterAI) -> None: assert obj.bar == 2 -async def test_async_response_parse_annotated_type(async_client: AsyncWriterAI) -> None: +async def test_async_response_parse_annotated_type(async_client: AsyncWriter) -> None: response = AsyncAPIResponse( raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), client=async_client, diff --git a/tests/test_streaming.py b/tests/test_streaming.py index 13dda1e2..1498f21b 100644 --- a/tests/test_streaming.py +++ b/tests/test_streaming.py @@ -5,13 +5,13 @@ import httpx import pytest -from writerai import WriterAI, AsyncWriterAI -from writerai._streaming import Stream, AsyncStream, ServerSentEvent +from writer import Writer, AsyncWriter +from writer._streaming import Stream, AsyncStream, ServerSentEvent @pytest.mark.asyncio @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_basic(sync: bool, client: WriterAI, async_client: AsyncWriterAI) -> None: +async def test_basic(sync: bool, client: Writer, async_client: AsyncWriter) -> None: def body() -> Iterator[bytes]: yield b"event: completion\n" yield b'data: {"foo":true}\n' @@ -28,7 +28,7 @@ def body() -> Iterator[bytes]: @pytest.mark.asyncio @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_data_missing_event(sync: bool, client: WriterAI, async_client: AsyncWriterAI) -> None: +async def test_data_missing_event(sync: bool, client: Writer, async_client: AsyncWriter) -> None: def body() -> Iterator[bytes]: yield b'data: {"foo":true}\n' yield b"\n" @@ -44,7 +44,7 @@ def body() -> Iterator[bytes]: @pytest.mark.asyncio @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_event_missing_data(sync: bool, client: WriterAI, async_client: AsyncWriterAI) -> None: +async def test_event_missing_data(sync: bool, client: Writer, async_client: AsyncWriter) -> None: def body() -> Iterator[bytes]: yield b"event: ping\n" yield b"\n" @@ -60,7 +60,7 @@ def body() -> Iterator[bytes]: @pytest.mark.asyncio @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_multiple_events(sync: bool, client: WriterAI, async_client: AsyncWriterAI) -> None: +async def test_multiple_events(sync: bool, client: Writer, async_client: AsyncWriter) -> None: def body() -> Iterator[bytes]: yield b"event: ping\n" yield b"\n" @@ -82,7 +82,7 @@ def body() -> Iterator[bytes]: @pytest.mark.asyncio @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_multiple_events_with_data(sync: bool, client: WriterAI, async_client: AsyncWriterAI) -> None: +async def test_multiple_events_with_data(sync: bool, client: Writer, async_client: AsyncWriter) -> None: def body() -> Iterator[bytes]: yield b"event: ping\n" yield b'data: {"foo":true}\n' @@ -106,7 +106,7 @@ def body() -> Iterator[bytes]: @pytest.mark.asyncio @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_multiple_data_lines_with_empty_line(sync: bool, client: WriterAI, async_client: AsyncWriterAI) -> None: +async def test_multiple_data_lines_with_empty_line(sync: bool, client: Writer, async_client: AsyncWriter) -> None: def body() -> Iterator[bytes]: yield b"event: ping\n" yield b"data: {\n" @@ -128,7 +128,7 @@ def body() -> Iterator[bytes]: @pytest.mark.asyncio @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_data_json_escaped_double_new_line(sync: bool, client: WriterAI, async_client: AsyncWriterAI) -> None: +async def test_data_json_escaped_double_new_line(sync: bool, client: Writer, async_client: AsyncWriter) -> None: def body() -> Iterator[bytes]: yield b"event: ping\n" yield b'data: {"foo": "my long\\n\\ncontent"}' @@ -145,7 +145,7 @@ def body() -> Iterator[bytes]: @pytest.mark.asyncio @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_multiple_data_lines(sync: bool, client: WriterAI, async_client: AsyncWriterAI) -> None: +async def test_multiple_data_lines(sync: bool, client: Writer, async_client: AsyncWriter) -> None: def body() -> Iterator[bytes]: yield b"event: ping\n" yield b"data: {\n" @@ -165,8 +165,8 @@ def body() -> Iterator[bytes]: @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) async def test_special_new_line_character( sync: bool, - client: WriterAI, - async_client: AsyncWriterAI, + client: Writer, + async_client: AsyncWriter, ) -> None: def body() -> Iterator[bytes]: yield b'data: {"content":" culpa"}\n' @@ -196,8 +196,8 @@ def body() -> Iterator[bytes]: @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) async def test_multi_byte_character_multiple_chunks( sync: bool, - client: WriterAI, - async_client: AsyncWriterAI, + client: Writer, + async_client: AsyncWriter, ) -> None: def body() -> Iterator[bytes]: yield b'data: {"content":"' @@ -237,8 +237,8 @@ def make_event_iterator( content: Iterator[bytes], *, sync: bool, - client: WriterAI, - async_client: AsyncWriterAI, + client: Writer, + async_client: AsyncWriter, ) -> Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]: if sync: return Stream(cast_to=object, client=client, response=httpx.Response(200, content=content))._iter_events() diff --git a/tests/test_transform.py b/tests/test_transform.py index 9e18f6fe..e989b0f2 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -8,15 +8,15 @@ import pytest -from writerai._types import Base64FileInput -from writerai._utils import ( +from writer._types import Base64FileInput +from writer._utils import ( PropertyInfo, transform as _transform, parse_datetime, async_transform as _async_transform, ) -from writerai._compat import PYDANTIC_V2 -from writerai._models import BaseModel +from writer._compat import PYDANTIC_V2 +from writer._models import BaseModel _T = TypeVar("_T") diff --git a/tests/test_utils/test_proxy.py b/tests/test_utils/test_proxy.py index 449f727d..5eac287f 100644 --- a/tests/test_utils/test_proxy.py +++ b/tests/test_utils/test_proxy.py @@ -2,7 +2,7 @@ from typing import Any from typing_extensions import override -from writerai._utils import LazyProxy +from writer._utils import LazyProxy class RecursiveLazyProxy(LazyProxy[Any]): diff --git a/tests/test_utils/test_typing.py b/tests/test_utils/test_typing.py index e0a437df..8b0ba02a 100644 --- a/tests/test_utils/test_typing.py +++ b/tests/test_utils/test_typing.py @@ -2,7 +2,7 @@ from typing import Generic, TypeVar, cast -from writerai._utils import extract_type_var_from_base +from writer._utils import extract_type_var_from_base _T = TypeVar("_T") _T2 = TypeVar("_T2") diff --git a/tests/utils.py b/tests/utils.py index 683796ce..9201bd38 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -8,8 +8,8 @@ from datetime import date, datetime from typing_extensions import Literal, get_args, get_origin, assert_type -from writerai._types import NoneType -from writerai._utils import ( +from writer._types import NoneType +from writer._utils import ( is_dict, is_list, is_list_type, @@ -17,8 +17,8 @@ extract_type_arg, is_annotated_type, ) -from writerai._compat import PYDANTIC_V2, field_outer_type, get_model_fields -from writerai._models import BaseModel +from writer._compat import PYDANTIC_V2, field_outer_type, get_model_fields +from writer._models import BaseModel BaseModelT = TypeVar("BaseModelT", bound=BaseModel) From d413b4e0d4cdf6580816d85d037d2f61722e6132 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 15 May 2024 13:20:26 +0000 Subject: [PATCH 010/399] chore: go live (#1) --- .github/workflows/publish-pypi.yml | 31 +++++++++++++ .github/workflows/release-doctor.yml | 19 ++++++++ .release-please-manifest.json | 3 ++ CONTRIBUTING.md | 4 +- README.md | 13 +++--- bin/check-release-environment | 32 ++++++++++++++ pyproject.toml | 6 +-- release-please-config.json | 66 ++++++++++++++++++++++++++++ src/writer/_version.py | 2 +- 9 files changed, 162 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/publish-pypi.yml create mode 100644 .github/workflows/release-doctor.yml create mode 100644 .release-please-manifest.json create mode 100644 bin/check-release-environment create mode 100644 release-please-config.json diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml new file mode 100644 index 00000000..03433c5e --- /dev/null +++ b/.github/workflows/publish-pypi.yml @@ -0,0 +1,31 @@ +# This workflow is triggered when a GitHub release is created. +# It can also be run manually to re-publish to PyPI in case it failed for some reason. +# You can run this workflow by navigating to https://www.github.com/WriterColab/sdk.python/actions/workflows/publish-pypi.yml +name: Publish PyPI +on: + workflow_dispatch: + + release: + types: [published] + +jobs: + publish: + name: publish + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install Rye + run: | + curl -sSf https://rye-up.com/get | bash + echo "$HOME/.rye/shims" >> $GITHUB_PATH + env: + RYE_VERSION: 0.24.0 + RYE_INSTALL_OPTION: "--yes" + + - name: Publish to PyPI + run: | + bash ./bin/publish-pypi + env: + PYPI_TOKEN: ${{ secrets.WRITER_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml new file mode 100644 index 00000000..2b77c3d2 --- /dev/null +++ b/.github/workflows/release-doctor.yml @@ -0,0 +1,19 @@ +name: Release Doctor +on: + pull_request: + workflow_dispatch: + +jobs: + release_doctor: + name: release doctor + runs-on: ubuntu-latest + if: github.repository == 'WriterColab/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 + + - name: Check release environment + run: | + bash ./bin/check-release-environment + env: + PYPI_TOKEN: ${{ secrets.WRITER_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 00000000..c4762802 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.0.1-alpha.0" +} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 57685f7e..ba5afbe4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,7 +59,7 @@ If you’d like to use the repository from source, you can either install from g To install via git: ```bash -pip install git+ssh://git@github.com/stainless-sdks/writer-python.git +pip install git+ssh://git@github.com/WriterColab/sdk.python.git ``` Alternatively, you can build from source and install the wheel file: @@ -117,7 +117,7 @@ the changes aren't made through the automated pipeline, you may want to make rel ### Publish with a GitHub workflow -You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/stainless-sdks/writer-python/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. +You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/WriterColab/sdk.python/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. ### Publish manually diff --git a/README.md b/README.md index 23bd7b66..3a4ad5f8 100644 --- a/README.md +++ b/README.md @@ -15,13 +15,10 @@ The REST API documentation can be found [on dev.writer.com](https://dev.writer.c ## Installation ```sh -# install from this staging repo -pip install git+ssh://git@github.com/stainless-sdks/writer-python.git +# install from PyPI +pip install --pre writer-sdk ``` -> [!NOTE] -> Once this package is [published to PyPI](https://app.stainlessapi.com/docs/guides/publish), this will become: `pip install --pre writer-sdk` - ## Usage The full API of this library can be found in [api.md](api.md). @@ -286,9 +283,9 @@ chat = response.parse() # get the object that `chat.chat()` would have returned print(chat.id) ``` -These methods return an [`APIResponse`](https://github.com/stainless-sdks/writer-python/tree/main/src/writer/_response.py) object. +These methods return an [`APIResponse`](https://github.com/WriterColab/sdk.python/tree/main/src/writer/_response.py) object. -The async client returns an [`AsyncAPIResponse`](https://github.com/stainless-sdks/writer-python/tree/main/src/writer/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. +The async client returns an [`AsyncAPIResponse`](https://github.com/WriterColab/sdk.python/tree/main/src/writer/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. #### `.with_streaming_response` @@ -384,7 +381,7 @@ This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) con We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. -We are keen for your feedback; please open an [issue](https://www.github.com/stainless-sdks/writer-python/issues) with questions, bugs, or suggestions. +We are keen for your feedback; please open an [issue](https://www.github.com/WriterColab/sdk.python/issues) with questions, bugs, or suggestions. ## Requirements diff --git a/bin/check-release-environment b/bin/check-release-environment new file mode 100644 index 00000000..360bcdd8 --- /dev/null +++ b/bin/check-release-environment @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +warnings=() +errors=() + +if [ -z "${PYPI_TOKEN}" ]; then + warnings+=("The WRITER_PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.") +fi + +lenWarnings=${#warnings[@]} + +if [[ lenWarnings -gt 0 ]]; then + echo -e "Found the following warnings in the release environment:\n" + + for warning in "${warnings[@]}"; do + echo -e "- $warning\n" + done +fi + +lenErrors=${#errors[@]} + +if [[ lenErrors -gt 0 ]]; then + echo -e "Found the following errors in the release environment:\n" + + for error in "${errors[@]}"; do + echo -e "- $error\n" + done + + exit 1 +fi + +echo "The environment is ready to push releases!" diff --git a/pyproject.toml b/pyproject.toml index c9702fd8..ac00174a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,8 +39,8 @@ classifiers = [ [project.urls] -Homepage = "https://github.com/stainless-sdks/writer-python" -Repository = "https://github.com/stainless-sdks/writer-python" +Homepage = "https://github.com/WriterColab/sdk.python" +Repository = "https://github.com/WriterColab/sdk.python" @@ -108,7 +108,7 @@ path = "README.md" [[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]] # replace relative links with absolute links pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)' -replacement = '[\1](https://github.com/stainless-sdks/writer-python/tree/main/\g<2>)' +replacement = '[\1](https://github.com/WriterColab/sdk.python/tree/main/\g<2>)' [tool.black] line-length = 120 diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 00000000..3edfd220 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,66 @@ +{ + "packages": { + ".": {} + }, + "$schema": "https://raw.githubusercontent.com/stainless-api/release-please/main/schemas/config.json", + "include-v-in-tag": true, + "include-component-in-tag": false, + "versioning": "prerelease", + "prerelease": true, + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": false, + "pull-request-header": "Automated Release PR", + "pull-request-title-pattern": "release: ${version}", + "changelog-sections": [ + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "perf", + "section": "Performance Improvements" + }, + { + "type": "revert", + "section": "Reverts" + }, + { + "type": "chore", + "section": "Chores" + }, + { + "type": "docs", + "section": "Documentation" + }, + { + "type": "style", + "section": "Styles" + }, + { + "type": "refactor", + "section": "Refactors" + }, + { + "type": "test", + "section": "Tests", + "hidden": true + }, + { + "type": "build", + "section": "Build System" + }, + { + "type": "ci", + "section": "Continuous Integration", + "hidden": true + } + ], + "release-type": "python", + "extra-files": [ + "src/writer/_version.py" + ] +} \ No newline at end of file diff --git a/src/writer/_version.py b/src/writer/_version.py index 3cd3f895..4f62e274 100644 --- a/src/writer/_version.py +++ b/src/writer/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writer" -__version__ = "0.0.1-alpha.0" +__version__ = "0.0.1-alpha.0" # x-release-please-version From 6203f389644f563155de0420b73028f60711925a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 15 May 2024 13:37:35 +0000 Subject: [PATCH 011/399] chore(internal): version bump (#3) --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/writer/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index c4762802..ba6c3483 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.0.1-alpha.0" + ".": "0.1.0-alpha.1" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index ac00174a..d6c23848 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "0.0.1-alpha.0" +version = "0.1.0-alpha.1" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writer/_version.py b/src/writer/_version.py index 4f62e274..bab3032c 100644 --- a/src/writer/_version.py +++ b/src/writer/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writer" -__version__ = "0.0.1-alpha.0" # x-release-please-version +__version__ = "0.1.0-alpha.1" # x-release-please-version From 6ad7bb8a876133af7e171efae0c685e0fb79cf88 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 16 May 2024 13:55:03 +0000 Subject: [PATCH 012/399] feat(api): update via SDK Studio (#4) --- CONTRIBUTING.md | 2 +- README.md | 36 +++++++++--------- api.md | 12 +++--- mypy.ini | 2 +- pyproject.toml | 6 +-- release-please-config.json | 2 +- scripts/lint | 2 +- src/{writer => writerai}/__init__.py | 4 +- src/{writer => writerai}/_base_client.py | 0 src/{writer => writerai}/_client.py | 0 src/{writer => writerai}/_compat.py | 0 src/{writer => writerai}/_constants.py | 0 src/{writer => writerai}/_exceptions.py | 0 src/{writer => writerai}/_files.py | 0 src/{writer => writerai}/_models.py | 0 src/{writer => writerai}/_qs.py | 0 src/{writer => writerai}/_resource.py | 0 src/{writer => writerai}/_response.py | 8 ++-- src/{writer => writerai}/_streaming.py | 0 src/{writer => writerai}/_types.py | 2 +- src/{writer => writerai}/_utils/__init__.py | 0 src/{writer => writerai}/_utils/_logs.py | 4 +- src/{writer => writerai}/_utils/_proxy.py | 0 src/{writer => writerai}/_utils/_streams.py | 0 src/{writer => writerai}/_utils/_sync.py | 0 src/{writer => writerai}/_utils/_transform.py | 0 src/{writer => writerai}/_utils/_typing.py | 0 src/{writer => writerai}/_utils/_utils.py | 0 src/{writer => writerai}/_version.py | 2 +- src/{writer => writerai}/py.typed | 0 .../resources/__init__.py | 0 src/{writer => writerai}/resources/chat.py | 0 .../resources/completions.py | 0 src/{writer => writerai}/resources/models.py | 0 src/{writer => writerai}/types/__init__.py | 0 src/{writer => writerai}/types/chat.py | 0 .../types/chat_chat_params.py | 0 .../types/chat_streaming_data.py | 0 src/{writer => writerai}/types/completion.py | 0 .../types/completion_create_params.py | 0 .../types/model_list_response.py | 0 .../types/streaming_data.py | 0 tests/api_resources/test_chat.py | 4 +- tests/api_resources/test_completions.py | 4 +- tests/api_resources/test_models.py | 4 +- tests/conftest.py | 4 +- tests/test_client.py | 37 +++++++++++-------- tests/test_deepcopy.py | 2 +- tests/test_extract_files.py | 4 +- tests/test_files.py | 2 +- tests/test_models.py | 6 +-- tests/test_qs.py | 2 +- tests/test_required_args.py | 2 +- tests/test_response.py | 14 +++---- tests/test_streaming.py | 4 +- tests/test_transform.py | 8 ++-- tests/test_utils/test_proxy.py | 2 +- tests/test_utils/test_typing.py | 2 +- tests/utils.py | 8 ++-- 59 files changed, 98 insertions(+), 93 deletions(-) rename src/{writer => writerai}/__init__.py (95%) rename src/{writer => writerai}/_base_client.py (100%) rename src/{writer => writerai}/_client.py (100%) rename src/{writer => writerai}/_compat.py (100%) rename src/{writer => writerai}/_constants.py (100%) rename src/{writer => writerai}/_exceptions.py (100%) rename src/{writer => writerai}/_files.py (100%) rename src/{writer => writerai}/_models.py (100%) rename src/{writer => writerai}/_qs.py (100%) rename src/{writer => writerai}/_resource.py (100%) rename src/{writer => writerai}/_response.py (99%) rename src/{writer => writerai}/_streaming.py (100%) rename src/{writer => writerai}/_types.py (99%) rename src/{writer => writerai}/_utils/__init__.py (100%) rename src/{writer => writerai}/_utils/_logs.py (76%) rename src/{writer => writerai}/_utils/_proxy.py (100%) rename src/{writer => writerai}/_utils/_streams.py (100%) rename src/{writer => writerai}/_utils/_sync.py (100%) rename src/{writer => writerai}/_utils/_transform.py (100%) rename src/{writer => writerai}/_utils/_typing.py (100%) rename src/{writer => writerai}/_utils/_utils.py (100%) rename src/{writer => writerai}/_version.py (86%) rename src/{writer => writerai}/py.typed (100%) rename src/{writer => writerai}/resources/__init__.py (100%) rename src/{writer => writerai}/resources/chat.py (100%) rename src/{writer => writerai}/resources/completions.py (100%) rename src/{writer => writerai}/resources/models.py (100%) rename src/{writer => writerai}/types/__init__.py (100%) rename src/{writer => writerai}/types/chat.py (100%) rename src/{writer => writerai}/types/chat_chat_params.py (100%) rename src/{writer => writerai}/types/chat_streaming_data.py (100%) rename src/{writer => writerai}/types/completion.py (100%) rename src/{writer => writerai}/types/completion_create_params.py (100%) rename src/{writer => writerai}/types/model_list_response.py (100%) rename src/{writer => writerai}/types/streaming_data.py (100%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ba5afbe4..63c29e02 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,7 +32,7 @@ $ pip install -r requirements-dev.lock ## Modifying/Adding code Most of the SDK is generated code, and any modified code will be overridden on the next generation. The -`src/writer/lib/` and `examples/` directories are exceptions and will never be overridden. +`src/writerai/lib/` and `examples/` directories are exceptions and will never be overridden. ## Adding and running examples diff --git a/README.md b/README.md index 3a4ad5f8..09ef07dd 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ The full API of this library can be found in [api.md](api.md). ```python import os -from writer import Writer +from writerai import Writer client = Writer( # This is the default and can be omitted @@ -56,7 +56,7 @@ Simply import `AsyncWriter` instead of `Writer` and use `await` with each API ca ```python import os import asyncio -from writer import AsyncWriter +from writerai import AsyncWriter client = AsyncWriter( # This is the default and can be omitted @@ -87,7 +87,7 @@ Functionality between the synchronous and asynchronous clients is otherwise iden We provide support for streaming responses using Server Side Events (SSE). ```python -from writer import Writer +from writerai import Writer client = Writer() @@ -103,7 +103,7 @@ for completion in stream: The async client uses the exact same interface. ```python -from writer import AsyncWriter +from writerai import AsyncWriter client = AsyncWriter() @@ -127,16 +127,16 @@ Typed requests and responses provide autocomplete and documentation within your ## Handling errors -When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `writer.APIConnectionError` is raised. +When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `writerai.APIConnectionError` is raised. When the API returns a non-success status code (that is, 4xx or 5xx -response), a subclass of `writer.APIStatusError` is raised, containing `status_code` and `response` properties. +response), a subclass of `writerai.APIStatusError` is raised, containing `status_code` and `response` properties. -All errors inherit from `writer.APIError`. +All errors inherit from `writerai.APIError`. ```python -import writer -from writer import Writer +import writerai +from writerai import Writer client = Writer() @@ -150,12 +150,12 @@ try: ], model="palmyra-x-chat-v2-32k", ) -except writer.APIConnectionError as e: +except writerai.APIConnectionError as e: print("The server could not be reached") print(e.__cause__) # an underlying Exception, likely raised within httpx. -except writer.RateLimitError as e: +except writerai.RateLimitError as e: print("A 429 status code was received; we should back off a bit.") -except writer.APIStatusError as e: +except writerai.APIStatusError as e: print("Another non-200-range status code was received") print(e.status_code) print(e.response) @@ -183,7 +183,7 @@ Connection errors (for example, due to a network connectivity problem), 408 Requ You can use the `max_retries` option to configure or disable retry settings: ```python -from writer import Writer +from writerai import Writer # Configure the default for all requests: client = Writer( @@ -209,7 +209,7 @@ By default requests time out after 1 minute. You can configure this with a `time which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/#fine-tuning-the-configuration) object: ```python -from writer import Writer +from writerai import Writer # Configure the default for all requests: client = Writer( @@ -267,7 +267,7 @@ if response.my_field is None: The "raw" Response object can be accessed by prefixing `.with_raw_response.` to any HTTP method call, e.g., ```py -from writer import Writer +from writerai import Writer client = Writer() response = client.chat.with_raw_response.chat( @@ -283,9 +283,9 @@ chat = response.parse() # get the object that `chat.chat()` would have returned print(chat.id) ``` -These methods return an [`APIResponse`](https://github.com/WriterColab/sdk.python/tree/main/src/writer/_response.py) object. +These methods return an [`APIResponse`](https://github.com/WriterColab/sdk.python/tree/main/src/writerai/_response.py) object. -The async client returns an [`AsyncAPIResponse`](https://github.com/WriterColab/sdk.python/tree/main/src/writer/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. +The async client returns an [`AsyncAPIResponse`](https://github.com/WriterColab/sdk.python/tree/main/src/writerai/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. #### `.with_streaming_response` @@ -355,7 +355,7 @@ You can directly override the [httpx client](https://www.python-httpx.org/api/#c - Additional [advanced](https://www.python-httpx.org/advanced/#client-instances) functionality ```python -from writer import Writer, DefaultHttpxClient +from writerai import Writer, DefaultHttpxClient client = Writer( # Or use the `WRITER_BASE_URL` env var diff --git a/api.md b/api.md index 190d95ac..0c42b4dc 100644 --- a/api.md +++ b/api.md @@ -3,33 +3,33 @@ Types: ```python -from writer.types import Chat, ChatStreamingData +from writerai.types import Chat, ChatStreamingData ``` Methods: -- client.chat.chat(\*\*params) -> Chat +- client.chat.chat(\*\*params) -> Chat # Completions Types: ```python -from writer.types import Completion, StreamingData +from writerai.types import Completion, StreamingData ``` Methods: -- client.completions.create(\*\*params) -> Completion +- client.completions.create(\*\*params) -> Completion # Models Types: ```python -from writer.types import ModelListResponse +from writerai.types import ModelListResponse ``` Methods: -- client.models.list() -> ModelListResponse +- client.models.list() -> ModelListResponse diff --git a/mypy.ini b/mypy.ini index 20d1f7ec..1ece7ab6 100644 --- a/mypy.ini +++ b/mypy.ini @@ -5,7 +5,7 @@ show_error_codes = True # Exclude _files.py because mypy isn't smart enough to apply # the correct type narrowing and as this is an internal module # it's fine to just use Pyright. -exclude = ^(src/writer/_files\.py|_dev/.*\.py)$ +exclude = ^(src/writerai/_files\.py|_dev/.*\.py)$ strict_equality = True implicit_reexport = True diff --git a/pyproject.toml b/pyproject.toml index d6c23848..46bf5d39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,7 +84,7 @@ typecheck = { chain = [ "typecheck:mypy" ]} "typecheck:pyright" = "pyright" -"typecheck:verify-types" = "pyright --verifytypes writer --ignoreexternal" +"typecheck:verify-types" = "pyright --verifytypes writerai --ignoreexternal" "typecheck:mypy" = "mypy ." [build-system] @@ -97,7 +97,7 @@ include = [ ] [tool.hatch.build.targets.wheel] -packages = ["src/writer"] +packages = ["src/writerai"] [tool.hatch.metadata.hooks.fancy-pypi-readme] content-type = "text/markdown" @@ -187,7 +187,7 @@ length-sort = true length-sort-straight = true combine-as-imports = true extra-standard-library = ["typing_extensions"] -known-first-party = ["writer", "tests"] +known-first-party = ["writerai", "tests"] [tool.ruff.per-file-ignores] "bin/**.py" = ["T201", "T203"] diff --git a/release-please-config.json b/release-please-config.json index 3edfd220..6b62e34d 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -61,6 +61,6 @@ ], "release-type": "python", "extra-files": [ - "src/writer/_version.py" + "src/writerai/_version.py" ] } \ No newline at end of file diff --git a/scripts/lint b/scripts/lint index 3260e8a0..5ecf173a 100755 --- a/scripts/lint +++ b/scripts/lint @@ -8,5 +8,5 @@ echo "==> Running lints" rye run lint echo "==> Making sure it imports" -rye run python -c 'import writer' +rye run python -c 'import writerai' diff --git a/src/writer/__init__.py b/src/writerai/__init__.py similarity index 95% rename from src/writer/__init__.py rename to src/writerai/__init__.py index 1b352daa..cc6410c2 100644 --- a/src/writer/__init__.py +++ b/src/writerai/__init__.py @@ -72,12 +72,12 @@ # Update the __module__ attribute for exported symbols so that # error messages point to this module instead of the module # it was originally defined in, e.g. -# writer._exceptions.NotFoundError -> writer.NotFoundError +# writerai._exceptions.NotFoundError -> writerai.NotFoundError __locals = locals() for __name in __all__: if not __name.startswith("__"): try: - __locals[__name].__module__ = "writer" + __locals[__name].__module__ = "writerai" except (TypeError, AttributeError): # Some of our exported symbols are builtins which we can't set attributes for. pass diff --git a/src/writer/_base_client.py b/src/writerai/_base_client.py similarity index 100% rename from src/writer/_base_client.py rename to src/writerai/_base_client.py diff --git a/src/writer/_client.py b/src/writerai/_client.py similarity index 100% rename from src/writer/_client.py rename to src/writerai/_client.py diff --git a/src/writer/_compat.py b/src/writerai/_compat.py similarity index 100% rename from src/writer/_compat.py rename to src/writerai/_compat.py diff --git a/src/writer/_constants.py b/src/writerai/_constants.py similarity index 100% rename from src/writer/_constants.py rename to src/writerai/_constants.py diff --git a/src/writer/_exceptions.py b/src/writerai/_exceptions.py similarity index 100% rename from src/writer/_exceptions.py rename to src/writerai/_exceptions.py diff --git a/src/writer/_files.py b/src/writerai/_files.py similarity index 100% rename from src/writer/_files.py rename to src/writerai/_files.py diff --git a/src/writer/_models.py b/src/writerai/_models.py similarity index 100% rename from src/writer/_models.py rename to src/writerai/_models.py diff --git a/src/writer/_qs.py b/src/writerai/_qs.py similarity index 100% rename from src/writer/_qs.py rename to src/writerai/_qs.py diff --git a/src/writer/_resource.py b/src/writerai/_resource.py similarity index 100% rename from src/writer/_resource.py rename to src/writerai/_resource.py diff --git a/src/writer/_response.py b/src/writerai/_response.py similarity index 99% rename from src/writer/_response.py rename to src/writerai/_response.py index ffbdee5f..f587c71b 100644 --- a/src/writer/_response.py +++ b/src/writerai/_response.py @@ -203,7 +203,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: return cast(R, response) if inspect.isclass(origin) and not issubclass(origin, BaseModel) and issubclass(origin, pydantic.BaseModel): - raise TypeError("Pydantic models must subclass our base model type, e.g. `from writer import BaseModel`") + raise TypeError("Pydantic models must subclass our base model type, e.g. `from writerai import BaseModel`") if ( cast_to is not object @@ -271,7 +271,7 @@ def parse(self, *, to: type[_T] | None = None) -> R | _T: the `to` argument, e.g. ```py - from writer import BaseModel + from writerai import BaseModel class MyModel(BaseModel): @@ -375,7 +375,7 @@ async def parse(self, *, to: type[_T] | None = None) -> R | _T: the `to` argument, e.g. ```py - from writer import BaseModel + from writerai import BaseModel class MyModel(BaseModel): @@ -546,7 +546,7 @@ async def stream_to_file( class MissingStreamClassError(TypeError): def __init__(self) -> None: super().__init__( - "The `stream` argument was set to `True` but the `stream_cls` argument was not given. See `writer._streaming` for reference", + "The `stream` argument was set to `True` but the `stream_cls` argument was not given. See `writerai._streaming` for reference", ) diff --git a/src/writer/_streaming.py b/src/writerai/_streaming.py similarity index 100% rename from src/writer/_streaming.py rename to src/writerai/_streaming.py diff --git a/src/writer/_types.py b/src/writerai/_types.py similarity index 99% rename from src/writer/_types.py rename to src/writerai/_types.py index 3cbb7cf3..7dbbaa54 100644 --- a/src/writer/_types.py +++ b/src/writerai/_types.py @@ -81,7 +81,7 @@ # This unfortunately means that you will either have # to import this type and pass it explicitly: # -# from writer import NoneType +# from writerai import NoneType # client.get('/foo', cast_to=NoneType) # # or build it yourself: diff --git a/src/writer/_utils/__init__.py b/src/writerai/_utils/__init__.py similarity index 100% rename from src/writer/_utils/__init__.py rename to src/writerai/_utils/__init__.py diff --git a/src/writer/_utils/_logs.py b/src/writerai/_utils/_logs.py similarity index 76% rename from src/writer/_utils/_logs.py rename to src/writerai/_utils/_logs.py index fc983487..f9529c8f 100644 --- a/src/writer/_utils/_logs.py +++ b/src/writerai/_utils/_logs.py @@ -1,12 +1,12 @@ import os import logging -logger: logging.Logger = logging.getLogger("writer") +logger: logging.Logger = logging.getLogger("writerai") httpx_logger: logging.Logger = logging.getLogger("httpx") def _basic_config() -> None: - # e.g. [2023-10-05 14:12:26 - writer._base_client:818 - DEBUG] HTTP Request: POST http://127.0.0.1:4010/foo/bar "200 OK" + # e.g. [2023-10-05 14:12:26 - writerai._base_client:818 - DEBUG] HTTP Request: POST http://127.0.0.1:4010/foo/bar "200 OK" logging.basicConfig( format="[%(asctime)s - %(name)s:%(lineno)d - %(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S", diff --git a/src/writer/_utils/_proxy.py b/src/writerai/_utils/_proxy.py similarity index 100% rename from src/writer/_utils/_proxy.py rename to src/writerai/_utils/_proxy.py diff --git a/src/writer/_utils/_streams.py b/src/writerai/_utils/_streams.py similarity index 100% rename from src/writer/_utils/_streams.py rename to src/writerai/_utils/_streams.py diff --git a/src/writer/_utils/_sync.py b/src/writerai/_utils/_sync.py similarity index 100% rename from src/writer/_utils/_sync.py rename to src/writerai/_utils/_sync.py diff --git a/src/writer/_utils/_transform.py b/src/writerai/_utils/_transform.py similarity index 100% rename from src/writer/_utils/_transform.py rename to src/writerai/_utils/_transform.py diff --git a/src/writer/_utils/_typing.py b/src/writerai/_utils/_typing.py similarity index 100% rename from src/writer/_utils/_typing.py rename to src/writerai/_utils/_typing.py diff --git a/src/writer/_utils/_utils.py b/src/writerai/_utils/_utils.py similarity index 100% rename from src/writer/_utils/_utils.py rename to src/writerai/_utils/_utils.py diff --git a/src/writer/_version.py b/src/writerai/_version.py similarity index 86% rename from src/writer/_version.py rename to src/writerai/_version.py index bab3032c..c6effcdc 100644 --- a/src/writer/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -__title__ = "writer" +__title__ = "writerai" __version__ = "0.1.0-alpha.1" # x-release-please-version diff --git a/src/writer/py.typed b/src/writerai/py.typed similarity index 100% rename from src/writer/py.typed rename to src/writerai/py.typed diff --git a/src/writer/resources/__init__.py b/src/writerai/resources/__init__.py similarity index 100% rename from src/writer/resources/__init__.py rename to src/writerai/resources/__init__.py diff --git a/src/writer/resources/chat.py b/src/writerai/resources/chat.py similarity index 100% rename from src/writer/resources/chat.py rename to src/writerai/resources/chat.py diff --git a/src/writer/resources/completions.py b/src/writerai/resources/completions.py similarity index 100% rename from src/writer/resources/completions.py rename to src/writerai/resources/completions.py diff --git a/src/writer/resources/models.py b/src/writerai/resources/models.py similarity index 100% rename from src/writer/resources/models.py rename to src/writerai/resources/models.py diff --git a/src/writer/types/__init__.py b/src/writerai/types/__init__.py similarity index 100% rename from src/writer/types/__init__.py rename to src/writerai/types/__init__.py diff --git a/src/writer/types/chat.py b/src/writerai/types/chat.py similarity index 100% rename from src/writer/types/chat.py rename to src/writerai/types/chat.py diff --git a/src/writer/types/chat_chat_params.py b/src/writerai/types/chat_chat_params.py similarity index 100% rename from src/writer/types/chat_chat_params.py rename to src/writerai/types/chat_chat_params.py diff --git a/src/writer/types/chat_streaming_data.py b/src/writerai/types/chat_streaming_data.py similarity index 100% rename from src/writer/types/chat_streaming_data.py rename to src/writerai/types/chat_streaming_data.py diff --git a/src/writer/types/completion.py b/src/writerai/types/completion.py similarity index 100% rename from src/writer/types/completion.py rename to src/writerai/types/completion.py diff --git a/src/writer/types/completion_create_params.py b/src/writerai/types/completion_create_params.py similarity index 100% rename from src/writer/types/completion_create_params.py rename to src/writerai/types/completion_create_params.py diff --git a/src/writer/types/model_list_response.py b/src/writerai/types/model_list_response.py similarity index 100% rename from src/writer/types/model_list_response.py rename to src/writerai/types/model_list_response.py diff --git a/src/writer/types/streaming_data.py b/src/writerai/types/streaming_data.py similarity index 100% rename from src/writer/types/streaming_data.py rename to src/writerai/types/streaming_data.py diff --git a/tests/api_resources/test_chat.py b/tests/api_resources/test_chat.py index 9c97c1b3..e6d7424d 100644 --- a/tests/api_resources/test_chat.py +++ b/tests/api_resources/test_chat.py @@ -7,9 +7,9 @@ import pytest -from writer import Writer, AsyncWriter +from writerai import Writer, AsyncWriter from tests.utils import assert_matches_type -from writer.types import Chat +from writerai.types import Chat base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") diff --git a/tests/api_resources/test_completions.py b/tests/api_resources/test_completions.py index 28086f47..d3b3a481 100644 --- a/tests/api_resources/test_completions.py +++ b/tests/api_resources/test_completions.py @@ -7,9 +7,9 @@ import pytest -from writer import Writer, AsyncWriter +from writerai import Writer, AsyncWriter from tests.utils import assert_matches_type -from writer.types import Completion +from writerai.types import Completion base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") diff --git a/tests/api_resources/test_models.py b/tests/api_resources/test_models.py index e7d78ff2..8ed984dc 100644 --- a/tests/api_resources/test_models.py +++ b/tests/api_resources/test_models.py @@ -7,9 +7,9 @@ import pytest -from writer import Writer, AsyncWriter +from writerai import Writer, AsyncWriter from tests.utils import assert_matches_type -from writer.types import ModelListResponse +from writerai.types import ModelListResponse base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") diff --git a/tests/conftest.py b/tests/conftest.py index 26f0840f..5acea6d5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,14 +7,14 @@ import pytest -from writer import Writer, AsyncWriter +from writerai import Writer, AsyncWriter if TYPE_CHECKING: from _pytest.fixtures import FixtureRequest pytest.register_assert_rewrite("tests.utils") -logging.getLogger("writer").setLevel(logging.DEBUG) +logging.getLogger("writerai").setLevel(logging.DEBUG) @pytest.fixture(scope="session") diff --git a/tests/test_client.py b/tests/test_client.py index b1cfd8f8..4048cfe1 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -16,12 +16,17 @@ from respx import MockRouter from pydantic import ValidationError -from writer import Writer, AsyncWriter, APIResponseValidationError -from writer._models import BaseModel, FinalRequestOptions -from writer._constants import RAW_RESPONSE_HEADER -from writer._streaming import Stream, AsyncStream -from writer._exceptions import WriterError, APIStatusError, APITimeoutError, APIResponseValidationError -from writer._base_client import DEFAULT_TIMEOUT, HTTPX_DEFAULT_TIMEOUT, BaseClient, make_request_options +from writerai import Writer, AsyncWriter, APIResponseValidationError +from writerai._models import BaseModel, FinalRequestOptions +from writerai._constants import RAW_RESPONSE_HEADER +from writerai._streaming import Stream, AsyncStream +from writerai._exceptions import WriterError, APIStatusError, APITimeoutError, APIResponseValidationError +from writerai._base_client import ( + DEFAULT_TIMEOUT, + HTTPX_DEFAULT_TIMEOUT, + BaseClient, + make_request_options, +) from .utils import update_env @@ -220,10 +225,10 @@ def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.Statistic # to_raw_response_wrapper leaks through the @functools.wraps() decorator. # # removing the decorator fixes the leak for reasons we don't understand. - "writer/_legacy_response.py", - "writer/_response.py", + "writerai/_legacy_response.py", + "writerai/_response.py", # pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason. - "writer/_compat.py", + "writerai/_compat.py", # Standard library leaks we don't care about. "/logging/__init__.py", ] @@ -706,7 +711,7 @@ def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str calculated = client._calculate_retry_timeout(remaining_retries, options, headers) assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType] - @mock.patch("writer._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @mock.patch("writerai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: respx_mock.post("/v1/chat").mock(side_effect=httpx.TimeoutException("Test timeout error")) @@ -732,7 +737,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> No assert _get_open_connections(self.client) == 0 - @mock.patch("writer._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @mock.patch("writerai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: respx_mock.post("/v1/chat").mock(return_value=httpx.Response(500)) @@ -934,10 +939,10 @@ def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.Statistic # to_raw_response_wrapper leaks through the @functools.wraps() decorator. # # removing the decorator fixes the leak for reasons we don't understand. - "writer/_legacy_response.py", - "writer/_response.py", + "writerai/_legacy_response.py", + "writerai/_response.py", # pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason. - "writer/_compat.py", + "writerai/_compat.py", # Standard library leaks we don't care about. "/logging/__init__.py", ] @@ -1437,7 +1442,7 @@ async def test_parse_retry_after_header(self, remaining_retries: int, retry_afte calculated = client._calculate_retry_timeout(remaining_retries, options, headers) assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType] - @mock.patch("writer._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @mock.patch("writerai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: respx_mock.post("/v1/chat").mock(side_effect=httpx.TimeoutException("Test timeout error")) @@ -1463,7 +1468,7 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) assert _get_open_connections(self.client) == 0 - @mock.patch("writer._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @mock.patch("writerai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: respx_mock.post("/v1/chat").mock(return_value=httpx.Response(500)) diff --git a/tests/test_deepcopy.py b/tests/test_deepcopy.py index 71f6fa67..628e4049 100644 --- a/tests/test_deepcopy.py +++ b/tests/test_deepcopy.py @@ -1,4 +1,4 @@ -from writer._utils import deepcopy_minimal +from writerai._utils import deepcopy_minimal def assert_different_identities(obj1: object, obj2: object) -> None: diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py index 7c1bf12f..9d9a4b19 100644 --- a/tests/test_extract_files.py +++ b/tests/test_extract_files.py @@ -4,8 +4,8 @@ import pytest -from writer._types import FileTypes -from writer._utils import extract_files +from writerai._types import FileTypes +from writerai._utils import extract_files def test_removes_files_from_input() -> None: diff --git a/tests/test_files.py b/tests/test_files.py index 1df82803..9cc66fe2 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -4,7 +4,7 @@ import pytest from dirty_equals import IsDict, IsList, IsBytes, IsTuple -from writer._files import to_httpx_files, async_to_httpx_files +from writerai._files import to_httpx_files, async_to_httpx_files readme_path = Path(__file__).parent.parent.joinpath("README.md") diff --git a/tests/test_models.py b/tests/test_models.py index 080df657..fa64230d 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -7,9 +7,9 @@ import pydantic from pydantic import Field -from writer._utils import PropertyInfo -from writer._compat import PYDANTIC_V2, parse_obj, model_dump, model_json -from writer._models import BaseModel, construct_type +from writerai._utils import PropertyInfo +from writerai._compat import PYDANTIC_V2, parse_obj, model_dump, model_json +from writerai._models import BaseModel, construct_type class BasicModel(BaseModel): diff --git a/tests/test_qs.py b/tests/test_qs.py index f49cc1b9..3dbda131 100644 --- a/tests/test_qs.py +++ b/tests/test_qs.py @@ -4,7 +4,7 @@ import pytest -from writer._qs import Querystring, stringify +from writerai._qs import Querystring, stringify def test_empty() -> None: diff --git a/tests/test_required_args.py b/tests/test_required_args.py index d2165d45..db5f699c 100644 --- a/tests/test_required_args.py +++ b/tests/test_required_args.py @@ -2,7 +2,7 @@ import pytest -from writer._utils import required_args +from writerai._utils import required_args def test_too_many_positional_params() -> None: diff --git a/tests/test_response.py b/tests/test_response.py index db65163a..0a4473fd 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -6,8 +6,8 @@ import pytest import pydantic -from writer import Writer, BaseModel, AsyncWriter -from writer._response import ( +from writerai import Writer, BaseModel, AsyncWriter +from writerai._response import ( APIResponse, BaseAPIResponse, AsyncAPIResponse, @@ -15,8 +15,8 @@ AsyncBinaryAPIResponse, extract_response_type, ) -from writer._streaming import Stream -from writer._base_client import FinalRequestOptions +from writerai._streaming import Stream +from writerai._base_client import FinalRequestOptions class ConcreteBaseAPIResponse(APIResponse[bytes]): @@ -40,7 +40,7 @@ def test_extract_response_type_direct_classes() -> None: def test_extract_response_type_direct_class_missing_type_arg() -> None: with pytest.raises( RuntimeError, - match="Expected type to have a type argument at index 0 but it did not", + match="Expected type to have a type argument at index 0 but it did not", ): extract_response_type(AsyncAPIResponse) @@ -72,7 +72,7 @@ def test_response_parse_mismatched_basemodel(client: Writer) -> None: with pytest.raises( TypeError, - match="Pydantic models must subclass our base model type, e.g. `from writer import BaseModel`", + match="Pydantic models must subclass our base model type, e.g. `from writerai import BaseModel`", ): response.parse(to=PydanticModel) @@ -90,7 +90,7 @@ async def test_async_response_parse_mismatched_basemodel(async_client: AsyncWrit with pytest.raises( TypeError, - match="Pydantic models must subclass our base model type, e.g. `from writer import BaseModel`", + match="Pydantic models must subclass our base model type, e.g. `from writerai import BaseModel`", ): await response.parse(to=PydanticModel) diff --git a/tests/test_streaming.py b/tests/test_streaming.py index 1498f21b..1c1f6d23 100644 --- a/tests/test_streaming.py +++ b/tests/test_streaming.py @@ -5,8 +5,8 @@ import httpx import pytest -from writer import Writer, AsyncWriter -from writer._streaming import Stream, AsyncStream, ServerSentEvent +from writerai import Writer, AsyncWriter +from writerai._streaming import Stream, AsyncStream, ServerSentEvent @pytest.mark.asyncio diff --git a/tests/test_transform.py b/tests/test_transform.py index e989b0f2..9e18f6fe 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -8,15 +8,15 @@ import pytest -from writer._types import Base64FileInput -from writer._utils import ( +from writerai._types import Base64FileInput +from writerai._utils import ( PropertyInfo, transform as _transform, parse_datetime, async_transform as _async_transform, ) -from writer._compat import PYDANTIC_V2 -from writer._models import BaseModel +from writerai._compat import PYDANTIC_V2 +from writerai._models import BaseModel _T = TypeVar("_T") diff --git a/tests/test_utils/test_proxy.py b/tests/test_utils/test_proxy.py index 5eac287f..449f727d 100644 --- a/tests/test_utils/test_proxy.py +++ b/tests/test_utils/test_proxy.py @@ -2,7 +2,7 @@ from typing import Any from typing_extensions import override -from writer._utils import LazyProxy +from writerai._utils import LazyProxy class RecursiveLazyProxy(LazyProxy[Any]): diff --git a/tests/test_utils/test_typing.py b/tests/test_utils/test_typing.py index 8b0ba02a..e0a437df 100644 --- a/tests/test_utils/test_typing.py +++ b/tests/test_utils/test_typing.py @@ -2,7 +2,7 @@ from typing import Generic, TypeVar, cast -from writer._utils import extract_type_var_from_base +from writerai._utils import extract_type_var_from_base _T = TypeVar("_T") _T2 = TypeVar("_T2") diff --git a/tests/utils.py b/tests/utils.py index 9201bd38..683796ce 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -8,8 +8,8 @@ from datetime import date, datetime from typing_extensions import Literal, get_args, get_origin, assert_type -from writer._types import NoneType -from writer._utils import ( +from writerai._types import NoneType +from writerai._utils import ( is_dict, is_list, is_list_type, @@ -17,8 +17,8 @@ extract_type_arg, is_annotated_type, ) -from writer._compat import PYDANTIC_V2, field_outer_type, get_model_fields -from writer._models import BaseModel +from writerai._compat import PYDANTIC_V2, field_outer_type, get_model_fields +from writerai._models import BaseModel BaseModelT = TypeVar("BaseModelT", bound=BaseModel) From 90f3dea5c57b14207f07881090bbcf3dd62857aa Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 17 May 2024 09:34:01 +0000 Subject: [PATCH 013/399] chore(internal): version bump (#6) --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index ba6c3483..f14b480a 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.1" + ".": "0.1.0-alpha.2" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 46bf5d39..e6dfbdec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "0.1.0-alpha.1" +version = "0.1.0-alpha.2" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index c6effcdc..052e1960 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "0.1.0-alpha.1" # x-release-please-version +__version__ = "0.1.0-alpha.2" # x-release-please-version From b584d446dd7a47d49dbb8635ad9787b23db5a66c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 24 May 2024 10:33:46 +0000 Subject: [PATCH 014/399] feat(api): update via SDK Studio (#7) --- .devcontainer/Dockerfile | 2 +- .github/workflows/ci.yml | 4 ++-- .github/workflows/publish-pypi.yml | 2 +- requirements-dev.lock | 2 +- src/writerai/_client.py | 4 ++-- src/writerai/_utils/_utils.py | 3 +-- 6 files changed, 8 insertions(+), 9 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index dd939620..83bca8f7 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -3,7 +3,7 @@ FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} USER vscode -RUN curl -sSf https://rye-up.com/get | RYE_VERSION="0.24.0" RYE_INSTALL_OPTION="--yes" bash +RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.24.0" RYE_INSTALL_OPTION="--yes" bash ENV PATH=/home/vscode/.rye/shims:$PATH RUN echo "[[ -d .venv ]] && source .venv/bin/activate" >> /home/vscode/.bashrc diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6fcd6aee..8c339440 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: - name: Install Rye run: | - curl -sSf https://rye-up.com/get | bash + curl -sSf https://rye.astral.sh/get | bash echo "$HOME/.rye/shims" >> $GITHUB_PATH env: RYE_VERSION: 0.24.0 @@ -38,7 +38,7 @@ jobs: - name: Install Rye run: | - curl -sSf https://rye-up.com/get | bash + curl -sSf https://rye.astral.sh/get | bash echo "$HOME/.rye/shims" >> $GITHUB_PATH env: RYE_VERSION: 0.24.0 diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 03433c5e..d20e98ba 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -18,7 +18,7 @@ jobs: - name: Install Rye run: | - curl -sSf https://rye-up.com/get | bash + curl -sSf https://rye.astral.sh/get | bash echo "$HOME/.rye/shims" >> $GITHUB_PATH env: RYE_VERSION: 0.24.0 diff --git a/requirements-dev.lock b/requirements-dev.lock index 752668e7..1b60f5e1 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -63,7 +63,7 @@ pydantic==2.7.1 # via writer-sdk pydantic-core==2.18.2 # via pydantic -pyright==1.1.359 +pyright==1.1.364 pytest==7.1.1 # via pytest-asyncio pytest-asyncio==0.21.1 diff --git a/src/writerai/_client.py b/src/writerai/_client.py index f5c3c805..97dc25bc 100644 --- a/src/writerai/_client.py +++ b/src/writerai/_client.py @@ -93,7 +93,7 @@ def __init__( if base_url is None: base_url = os.environ.get("WRITER_BASE_URL") if base_url is None: - base_url = f"https://api.qordobadev.com" + base_url = f"https://api.writer.com" super().__init__( version=__version__, @@ -267,7 +267,7 @@ def __init__( if base_url is None: base_url = os.environ.get("WRITER_BASE_URL") if base_url is None: - base_url = f"https://api.qordobadev.com" + base_url = f"https://api.writer.com" super().__init__( version=__version__, diff --git a/src/writerai/_utils/_utils.py b/src/writerai/_utils/_utils.py index 17904ce6..34797c29 100644 --- a/src/writerai/_utils/_utils.py +++ b/src/writerai/_utils/_utils.py @@ -20,7 +20,7 @@ import sniffio -from .._types import Headers, NotGiven, FileTypes, NotGivenOr, HeadersLike +from .._types import NotGiven, FileTypes, NotGivenOr, HeadersLike from .._compat import parse_date as parse_date, parse_datetime as parse_datetime _T = TypeVar("_T") @@ -370,7 +370,6 @@ def file_from_path(path: str) -> FileTypes: def get_required_header(headers: HeadersLike, header: str) -> str: lower_header = header.lower() if isinstance(headers, Mapping): - headers = cast(Headers, headers) for k, v in headers.items(): if k.lower() == lower_header and isinstance(v, str): return v From adb50ad4cf6640298532ef204dd1208d3587991d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 24 May 2024 11:19:46 +0000 Subject: [PATCH 015/399] chore(internal): version bump (#9) --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index f14b480a..aaf968a1 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.2" + ".": "0.1.0-alpha.3" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index e6dfbdec..5e57313b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "0.1.0-alpha.2" +version = "0.1.0-alpha.3" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index 052e1960..a25555d3 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "0.1.0-alpha.2" # x-release-please-version +__version__ = "0.1.0-alpha.3" # x-release-please-version From 37c3746da38f09f25a84077adfa8a9ba9d8fea21 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 4 Jun 2024 01:25:25 +0000 Subject: [PATCH 016/399] feat(api): update via SDK Studio (#10) --- CONTRIBUTING.md | 2 +- README.md | 14 +++++++------- scripts/bootstrap | 2 +- tests/test_client.py | 8 ++++---- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 63c29e02..c91e2137 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ ### With Rye -We use [Rye](https://rye-up.com/) to manage dependencies so we highly recommend [installing it](https://rye-up.com/guide/installation/) as it will automatically provision a Python environment with the expected Python version. +We use [Rye](https://rye.astral.sh/) to manage dependencies so we highly recommend [installing it](https://rye.astral.sh/guide/installation/) as it will automatically provision a Python environment with the expected Python version. After installing Rye, you'll just have to run this command: diff --git a/README.md b/README.md index 09ef07dd..b8a2f3b0 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ chat = client.chat.chat( "role": "user", } ], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-v2", ) print(chat.id) ``` @@ -72,7 +72,7 @@ async def main() -> None: "role": "user", } ], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-v2", ) print(chat.id) @@ -148,7 +148,7 @@ try: "role": "user", } ], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-v2", ) except writerai.APIConnectionError as e: print("The server could not be reached") @@ -199,7 +199,7 @@ client.with_options(max_retries=5).chat.chat( "role": "user", } ], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-v2", ) ``` @@ -230,7 +230,7 @@ client.with_options(timeout=5.0).chat.chat( "role": "user", } ], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-v2", ) ``` @@ -275,7 +275,7 @@ response = client.chat.with_raw_response.chat( "content": "Hello!", "role": "user", }], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-v2", ) print(response.headers.get('X-My-Header')) @@ -301,7 +301,7 @@ with client.chat.with_streaming_response.chat( "role": "user", } ], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-v2", ) as response: print(response.headers.get("X-My-Header")) diff --git a/scripts/bootstrap b/scripts/bootstrap index 29df07e7..8c5c60eb 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -16,4 +16,4 @@ echo "==> Installing Python dependencies…" # experimental uv support makes installations significantly faster rye config --set-bool behavior.use-uv=true -rye sync +rye sync --all-features diff --git a/tests/test_client.py b/tests/test_client.py index 4048cfe1..868ffce7 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -728,7 +728,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> No "role": "user", } ], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-v2", ), ), cast_to=httpx.Response, @@ -754,7 +754,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> Non "role": "user", } ], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-v2", ), ), cast_to=httpx.Response, @@ -1459,7 +1459,7 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) "role": "user", } ], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-v2", ), ), cast_to=httpx.Response, @@ -1485,7 +1485,7 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) "role": "user", } ], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-v2", ), ), cast_to=httpx.Response, From ce1c4d889e25327db1ca9bf62e40062a2f6fb55a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 4 Jun 2024 01:26:38 +0000 Subject: [PATCH 017/399] feat(api): update via SDK Studio (#12) --- README.md | 14 +++++++------- tests/test_client.py | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index b8a2f3b0..5660f6b5 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ chat = client.chat.chat( "role": "user", } ], - model="palmyra-x-v2", + model="palmyra-chat-v2-32k", ) print(chat.id) ``` @@ -72,7 +72,7 @@ async def main() -> None: "role": "user", } ], - model="palmyra-x-v2", + model="palmyra-chat-v2-32k", ) print(chat.id) @@ -148,7 +148,7 @@ try: "role": "user", } ], - model="palmyra-x-v2", + model="[object Object]", ) except writerai.APIConnectionError as e: print("The server could not be reached") @@ -199,7 +199,7 @@ client.with_options(max_retries=5).chat.chat( "role": "user", } ], - model="palmyra-x-v2", + model="[object Object]", ) ``` @@ -230,7 +230,7 @@ client.with_options(timeout=5.0).chat.chat( "role": "user", } ], - model="palmyra-x-v2", + model="[object Object]", ) ``` @@ -275,7 +275,7 @@ response = client.chat.with_raw_response.chat( "content": "Hello!", "role": "user", }], - model="palmyra-x-v2", + model="[object Object]", ) print(response.headers.get('X-My-Header')) @@ -301,7 +301,7 @@ with client.chat.with_streaming_response.chat( "role": "user", } ], - model="palmyra-x-v2", + model="[object Object]", ) as response: print(response.headers.get("X-My-Header")) diff --git a/tests/test_client.py b/tests/test_client.py index 868ffce7..9e1dbf58 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -728,7 +728,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> No "role": "user", } ], - model="palmyra-x-v2", + model="[object Object]", ), ), cast_to=httpx.Response, @@ -754,7 +754,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> Non "role": "user", } ], - model="palmyra-x-v2", + model="[object Object]", ), ), cast_to=httpx.Response, @@ -1459,7 +1459,7 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) "role": "user", } ], - model="palmyra-x-v2", + model="[object Object]", ), ), cast_to=httpx.Response, @@ -1485,7 +1485,7 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) "role": "user", } ], - model="palmyra-x-v2", + model="[object Object]", ), ), cast_to=httpx.Response, From 712053a94a8fea654e4f284a36100afd64eed90e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 4 Jun 2024 01:27:38 +0000 Subject: [PATCH 018/399] feat(api): update via SDK Studio (#13) --- README.md | 10 +++++----- tests/test_client.py | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 5660f6b5..31ff1ffe 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ try: "role": "user", } ], - model="[object Object]", + model="palmyra-x-chat-v2-32k", ) except writerai.APIConnectionError as e: print("The server could not be reached") @@ -199,7 +199,7 @@ client.with_options(max_retries=5).chat.chat( "role": "user", } ], - model="[object Object]", + model="palmyra-x-chat-v2-32k", ) ``` @@ -230,7 +230,7 @@ client.with_options(timeout=5.0).chat.chat( "role": "user", } ], - model="[object Object]", + model="palmyra-x-chat-v2-32k", ) ``` @@ -275,7 +275,7 @@ response = client.chat.with_raw_response.chat( "content": "Hello!", "role": "user", }], - model="[object Object]", + model="palmyra-x-chat-v2-32k", ) print(response.headers.get('X-My-Header')) @@ -301,7 +301,7 @@ with client.chat.with_streaming_response.chat( "role": "user", } ], - model="[object Object]", + model="palmyra-x-chat-v2-32k", ) as response: print(response.headers.get("X-My-Header")) diff --git a/tests/test_client.py b/tests/test_client.py index 9e1dbf58..4048cfe1 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -728,7 +728,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> No "role": "user", } ], - model="[object Object]", + model="palmyra-x-chat-v2-32k", ), ), cast_to=httpx.Response, @@ -754,7 +754,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> Non "role": "user", } ], - model="[object Object]", + model="palmyra-x-chat-v2-32k", ), ), cast_to=httpx.Response, @@ -1459,7 +1459,7 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) "role": "user", } ], - model="[object Object]", + model="palmyra-x-chat-v2-32k", ), ), cast_to=httpx.Response, @@ -1485,7 +1485,7 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) "role": "user", } ], - model="[object Object]", + model="palmyra-x-chat-v2-32k", ), ), cast_to=httpx.Response, From 8ea393fed460bdd741996d01b6eb0337c4519a2d Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Tue, 4 Jun 2024 01:38:11 +0000 Subject: [PATCH 019/399] feat(api): update via SDK Studio --- README.md | 18 +++++++++--------- tests/test_client.py | 8 ++++---- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 31ff1ffe..b8f8dd89 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ chat = client.chat.chat( "role": "user", } ], - model="palmyra-chat-v2-32k", + model="palmyra-x-32k", ) print(chat.id) ``` @@ -72,7 +72,7 @@ async def main() -> None: "role": "user", } ], - model="palmyra-chat-v2-32k", + model="palmyra-x-32k", ) print(chat.id) @@ -92,7 +92,7 @@ from writerai import Writer client = Writer() stream = client.completions.create( - model="palmyra-x-v2", + model="palmyra-x-32k", prompt="Hi, my name is", stream=True, ) @@ -108,7 +108,7 @@ from writerai import AsyncWriter client = AsyncWriter() stream = await client.completions.create( - model="palmyra-x-v2", + model="palmyra-x-32k", prompt="Hi, my name is", stream=True, ) @@ -148,7 +148,7 @@ try: "role": "user", } ], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-32k", ) except writerai.APIConnectionError as e: print("The server could not be reached") @@ -199,7 +199,7 @@ client.with_options(max_retries=5).chat.chat( "role": "user", } ], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-32k", ) ``` @@ -230,7 +230,7 @@ client.with_options(timeout=5.0).chat.chat( "role": "user", } ], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-32k", ) ``` @@ -275,7 +275,7 @@ response = client.chat.with_raw_response.chat( "content": "Hello!", "role": "user", }], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-32k", ) print(response.headers.get('X-My-Header')) @@ -301,7 +301,7 @@ with client.chat.with_streaming_response.chat( "role": "user", } ], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-32k", ) as response: print(response.headers.get("X-My-Header")) diff --git a/tests/test_client.py b/tests/test_client.py index 4048cfe1..829fd508 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -728,7 +728,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> No "role": "user", } ], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-32k", ), ), cast_to=httpx.Response, @@ -754,7 +754,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> Non "role": "user", } ], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-32k", ), ), cast_to=httpx.Response, @@ -1459,7 +1459,7 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) "role": "user", } ], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-32k", ), ), cast_to=httpx.Response, @@ -1485,7 +1485,7 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) "role": "user", } ], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-32k", ), ), cast_to=httpx.Response, From 243ea9a86c834dfc6c6a32d8e1ec7567d5a6bdd4 Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Tue, 4 Jun 2024 14:26:08 +0000 Subject: [PATCH 020/399] feat(api): update via SDK Studio --- .github/workflows/create-releases.yml | 38 +++++++++++++++++++ .../handle-release-pr-title-edit.yml | 25 ++++++++++++ .github/workflows/publish-pypi.yml | 8 +--- .github/workflows/release-doctor.yml | 1 + bin/check-release-environment | 4 ++ 5 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/create-releases.yml create mode 100644 .github/workflows/handle-release-pr-title-edit.yml diff --git a/.github/workflows/create-releases.yml b/.github/workflows/create-releases.yml new file mode 100644 index 00000000..923abae1 --- /dev/null +++ b/.github/workflows/create-releases.yml @@ -0,0 +1,38 @@ +name: Create releases +on: + schedule: + - cron: '0 5 * * *' # every day at 5am UTC + push: + branches: + - main + +jobs: + release: + name: release + if: github.ref == 'refs/heads/main' && github.repository == 'WriterColab/sdk.python' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: stainless-api/trigger-release-please@v1 + id: release + with: + repo: ${{ github.event.repository.full_name }} + stainless-api-key: ${{ secrets.STAINLESS_API_KEY }} + + - name: Install Rye + if: ${{ steps.release.outputs.releases_created }} + run: | + curl -sSf https://rye.astral.sh/get | bash + echo "$HOME/.rye/shims" >> $GITHUB_PATH + env: + RYE_VERSION: 0.24.0 + RYE_INSTALL_OPTION: "--yes" + + - name: Publish to PyPI + if: ${{ steps.release.outputs.releases_created }} + run: | + bash ./bin/publish-pypi + env: + PYPI_TOKEN: ${{ secrets.WRITER_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.github/workflows/handle-release-pr-title-edit.yml b/.github/workflows/handle-release-pr-title-edit.yml new file mode 100644 index 00000000..e267b5c9 --- /dev/null +++ b/.github/workflows/handle-release-pr-title-edit.yml @@ -0,0 +1,25 @@ +name: Handle release PR title edits +on: + pull_request: + types: + - edited + - unlabeled + +jobs: + update_pr_content: + name: Update pull request content + if: | + ((github.event.action == 'edited' && github.event.changes.title.from != github.event.pull_request.title) || + (github.event.action == 'unlabeled' && github.event.label.name == 'autorelease: custom version')) && + startsWith(github.event.pull_request.head.ref, 'release-please--') && + github.event.pull_request.state == 'open' && + github.event.sender.login != 'stainless-bot' && + github.event.sender.login != 'stainless-app' && + github.repository == 'WriterColab/sdk.python' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: stainless-api/trigger-release-please@v1 + with: + repo: ${{ github.event.repository.full_name }} + stainless-api-key: ${{ secrets.STAINLESS_API_KEY }} diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index d20e98ba..9a1a3258 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -1,13 +1,9 @@ -# This workflow is triggered when a GitHub release is created. -# It can also be run manually to re-publish to PyPI in case it failed for some reason. -# You can run this workflow by navigating to https://www.github.com/WriterColab/sdk.python/actions/workflows/publish-pypi.yml +# workflow for re-running publishing to PyPI in case it fails for some reason +# you can run this workflow by navigating to https://www.github.com/WriterColab/sdk.python/actions/workflows/publish-pypi.yml name: Publish PyPI on: workflow_dispatch: - release: - types: [published] - jobs: publish: name: publish diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index 2b77c3d2..839b250e 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -16,4 +16,5 @@ jobs: run: | bash ./bin/check-release-environment env: + STAINLESS_API_KEY: ${{ secrets.STAINLESS_API_KEY }} PYPI_TOKEN: ${{ secrets.WRITER_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/bin/check-release-environment b/bin/check-release-environment index 360bcdd8..e5042401 100644 --- a/bin/check-release-environment +++ b/bin/check-release-environment @@ -3,6 +3,10 @@ warnings=() errors=() +if [ -z "${STAINLESS_API_KEY}" ]; then + errors+=("The STAINLESS_API_KEY secret has not been set. Please contact Stainless for an API key & set it in your organization secrets on GitHub.") +fi + if [ -z "${PYPI_TOKEN}" ]; then warnings+=("The WRITER_PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.") fi From bdfcb9103213bc2321610ef2bd39a540c03e14d1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 4 Jun 2024 14:27:25 +0000 Subject: [PATCH 021/399] chore: go live (#2) --- .github/workflows/create-releases.yml | 38 ------------------- .../handle-release-pr-title-edit.yml | 25 ------------ .github/workflows/publish-pypi.yml | 8 +++- .github/workflows/release-doctor.yml | 3 +- CONTRIBUTING.md | 4 +- README.md | 6 +-- bin/check-release-environment | 4 -- pyproject.toml | 6 +-- 8 files changed, 15 insertions(+), 79 deletions(-) delete mode 100644 .github/workflows/create-releases.yml delete mode 100644 .github/workflows/handle-release-pr-title-edit.yml diff --git a/.github/workflows/create-releases.yml b/.github/workflows/create-releases.yml deleted file mode 100644 index 923abae1..00000000 --- a/.github/workflows/create-releases.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Create releases -on: - schedule: - - cron: '0 5 * * *' # every day at 5am UTC - push: - branches: - - main - -jobs: - release: - name: release - if: github.ref == 'refs/heads/main' && github.repository == 'WriterColab/sdk.python' - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - uses: stainless-api/trigger-release-please@v1 - id: release - with: - repo: ${{ github.event.repository.full_name }} - stainless-api-key: ${{ secrets.STAINLESS_API_KEY }} - - - name: Install Rye - if: ${{ steps.release.outputs.releases_created }} - run: | - curl -sSf https://rye.astral.sh/get | bash - echo "$HOME/.rye/shims" >> $GITHUB_PATH - env: - RYE_VERSION: 0.24.0 - RYE_INSTALL_OPTION: "--yes" - - - name: Publish to PyPI - if: ${{ steps.release.outputs.releases_created }} - run: | - bash ./bin/publish-pypi - env: - PYPI_TOKEN: ${{ secrets.WRITER_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.github/workflows/handle-release-pr-title-edit.yml b/.github/workflows/handle-release-pr-title-edit.yml deleted file mode 100644 index e267b5c9..00000000 --- a/.github/workflows/handle-release-pr-title-edit.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Handle release PR title edits -on: - pull_request: - types: - - edited - - unlabeled - -jobs: - update_pr_content: - name: Update pull request content - if: | - ((github.event.action == 'edited' && github.event.changes.title.from != github.event.pull_request.title) || - (github.event.action == 'unlabeled' && github.event.label.name == 'autorelease: custom version')) && - startsWith(github.event.pull_request.head.ref, 'release-please--') && - github.event.pull_request.state == 'open' && - github.event.sender.login != 'stainless-bot' && - github.event.sender.login != 'stainless-app' && - github.repository == 'WriterColab/sdk.python' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: stainless-api/trigger-release-please@v1 - with: - repo: ${{ github.event.repository.full_name }} - stainless-api-key: ${{ secrets.STAINLESS_API_KEY }} diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 9a1a3258..b850d240 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -1,9 +1,13 @@ -# workflow for re-running publishing to PyPI in case it fails for some reason -# you can run this workflow by navigating to https://www.github.com/WriterColab/sdk.python/actions/workflows/publish-pypi.yml +# This workflow is triggered when a GitHub release is created. +# It can also be run manually to re-publish to PyPI in case it failed for some reason. +# You can run this workflow by navigating to https://www.github.com/writerai/writer-python/actions/workflows/publish-pypi.yml name: Publish PyPI on: workflow_dispatch: + release: + types: [published] + jobs: publish: name: publish diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index 839b250e..33a5d613 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -7,7 +7,7 @@ jobs: release_doctor: name: release doctor runs-on: ubuntu-latest - if: github.repository == 'WriterColab/sdk.python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') + if: github.repository == 'writerai/writer-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 @@ -16,5 +16,4 @@ jobs: run: | bash ./bin/check-release-environment env: - STAINLESS_API_KEY: ${{ secrets.STAINLESS_API_KEY }} PYPI_TOKEN: ${{ secrets.WRITER_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c91e2137..d41926b0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,7 +59,7 @@ If you’d like to use the repository from source, you can either install from g To install via git: ```bash -pip install git+ssh://git@github.com/WriterColab/sdk.python.git +pip install git+ssh://git@github.com/writerai/writer-python.git ``` Alternatively, you can build from source and install the wheel file: @@ -117,7 +117,7 @@ the changes aren't made through the automated pipeline, you may want to make rel ### Publish with a GitHub workflow -You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/WriterColab/sdk.python/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. +You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/writerai/writer-python/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. ### Publish manually diff --git a/README.md b/README.md index b8f8dd89..9159794e 100644 --- a/README.md +++ b/README.md @@ -283,9 +283,9 @@ chat = response.parse() # get the object that `chat.chat()` would have returned print(chat.id) ``` -These methods return an [`APIResponse`](https://github.com/WriterColab/sdk.python/tree/main/src/writerai/_response.py) object. +These methods return an [`APIResponse`](https://github.com/writerai/writer-python/tree/main/src/writerai/_response.py) object. -The async client returns an [`AsyncAPIResponse`](https://github.com/WriterColab/sdk.python/tree/main/src/writerai/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. +The async client returns an [`AsyncAPIResponse`](https://github.com/writerai/writer-python/tree/main/src/writerai/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. #### `.with_streaming_response` @@ -381,7 +381,7 @@ This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) con We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. -We are keen for your feedback; please open an [issue](https://www.github.com/WriterColab/sdk.python/issues) with questions, bugs, or suggestions. +We are keen for your feedback; please open an [issue](https://www.github.com/writerai/writer-python/issues) with questions, bugs, or suggestions. ## Requirements diff --git a/bin/check-release-environment b/bin/check-release-environment index e5042401..360bcdd8 100644 --- a/bin/check-release-environment +++ b/bin/check-release-environment @@ -3,10 +3,6 @@ warnings=() errors=() -if [ -z "${STAINLESS_API_KEY}" ]; then - errors+=("The STAINLESS_API_KEY secret has not been set. Please contact Stainless for an API key & set it in your organization secrets on GitHub.") -fi - if [ -z "${PYPI_TOKEN}" ]; then warnings+=("The WRITER_PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.") fi diff --git a/pyproject.toml b/pyproject.toml index 5e57313b..04c3e8e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,8 +39,8 @@ classifiers = [ [project.urls] -Homepage = "https://github.com/WriterColab/sdk.python" -Repository = "https://github.com/WriterColab/sdk.python" +Homepage = "https://github.com/writerai/writer-python" +Repository = "https://github.com/writerai/writer-python" @@ -108,7 +108,7 @@ path = "README.md" [[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]] # replace relative links with absolute links pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)' -replacement = '[\1](https://github.com/WriterColab/sdk.python/tree/main/\g<2>)' +replacement = '[\1](https://github.com/writerai/writer-python/tree/main/\g<2>)' [tool.black] line-length = 120 From a8a212171af444ed87779e23e471a7649f2d1467 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 4 Jun 2024 18:08:22 +0000 Subject: [PATCH 022/399] feat(api): update via SDK Studio (#3) --- .stats.yml | 2 +- README.md | 14 +- src/writerai/resources/chat.py | 198 +++++++++++++++++- src/writerai/resources/completions.py | 174 ++++++++++++++- src/writerai/resources/models.py | 4 +- src/writerai/types/chat.py | 31 +++ src/writerai/types/chat_chat_params.py | 44 ++++ src/writerai/types/completion.py | 26 +++ .../types/completion_create_params.py | 37 ++++ src/writerai/types/model_list_response.py | 3 + tests/api_resources/test_chat.py | 64 +++--- tests/api_resources/test_completions.py | 112 +++++----- tests/test_client.py | 8 +- 13 files changed, 603 insertions(+), 114 deletions(-) diff --git a/.stats.yml b/.stats.yml index 701fc274..0b0545e7 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 3 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-e5ad2fb12fbda084403c1696af9dbe7eeb5f0025134473dea7632339d4d7d00b.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-387e688cfbf5098041d47c9c918c15d4978f98768b4daf901267aea8affc0a30.yml diff --git a/README.md b/README.md index 9159794e..f768394b 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ client = Writer( chat = client.chat.chat( messages=[ { - "content": "Hello!", + "content": "string", "role": "user", } ], @@ -68,7 +68,7 @@ async def main() -> None: chat = await client.chat.chat( messages=[ { - "content": "Hello!", + "content": "string", "role": "user", } ], @@ -144,7 +144,7 @@ try: client.chat.chat( messages=[ { - "content": "Hello!", + "content": "string", "role": "user", } ], @@ -195,7 +195,7 @@ client = Writer( client.with_options(max_retries=5).chat.chat( messages=[ { - "content": "Hello!", + "content": "string", "role": "user", } ], @@ -226,7 +226,7 @@ client = Writer( client.with_options(timeout=5.0).chat.chat( messages=[ { - "content": "Hello!", + "content": "string", "role": "user", } ], @@ -272,7 +272,7 @@ from writerai import Writer client = Writer() response = client.chat.with_raw_response.chat( messages=[{ - "content": "Hello!", + "content": "string", "role": "user", }], model="palmyra-x-32k", @@ -297,7 +297,7 @@ To stream the response body, use `.with_streaming_response` instead, which requi with client.chat.with_streaming_response.chat( messages=[ { - "content": "Hello!", + "content": "string", "role": "user", } ], diff --git a/src/writerai/resources/chat.py b/src/writerai/resources/chat.py index 742b950a..c2f21f1d 100644 --- a/src/writerai/resources/chat.py +++ b/src/writerai/resources/chat.py @@ -61,9 +61,40 @@ def chat( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> Chat: """ - Create chat completion + Chat completion Args: + messages: An array of message objects that form the conversation history or context for + the model to respond to. The array must contain at least one message. + + model: Specifies the model to be used for generating responses. The chat model is + always `palmyra-x-002-32k` for conversational use. + + max_tokens: Defines the maximum number of tokens (words and characters) that the model can + generate in the response. The default value is set to 16, but it can be adjusted + to allow for longer or shorter responses as needed. + + n: Specifies the number of completions (responses) to generate from the model in a + single request. This parameter allows multiple responses to be generated, + offering a variety of potential replies from which to choose. + + stop: A token or sequence of tokens that, when generated, will cause the model to stop + producing further content. This can be a single token or an array of tokens, + acting as a signal to end the output. + + stream: Indicates whether the response should be streamed incrementally as it is + generated or only returned once fully complete. Streaming can be useful for + providing real-time feedback in interactive applications. + + temperature: Controls the randomness or creativity of the model's responses. A higher + temperature results in more varied and less predictable text, while a lower + temperature produces more deterministic and conservative outputs. + + top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's + token generation on the most likely subset of tokens. Only tokens with + cumulative probability above this threshold are considered, controlling the + trade-off between creativity and coherence. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -94,9 +125,40 @@ def chat( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> Stream[ChatStreamingData]: """ - Create chat completion + Chat completion Args: + messages: An array of message objects that form the conversation history or context for + the model to respond to. The array must contain at least one message. + + model: Specifies the model to be used for generating responses. The chat model is + always `palmyra-x-002-32k` for conversational use. + + stream: Indicates whether the response should be streamed incrementally as it is + generated or only returned once fully complete. Streaming can be useful for + providing real-time feedback in interactive applications. + + max_tokens: Defines the maximum number of tokens (words and characters) that the model can + generate in the response. The default value is set to 16, but it can be adjusted + to allow for longer or shorter responses as needed. + + n: Specifies the number of completions (responses) to generate from the model in a + single request. This parameter allows multiple responses to be generated, + offering a variety of potential replies from which to choose. + + stop: A token or sequence of tokens that, when generated, will cause the model to stop + producing further content. This can be a single token or an array of tokens, + acting as a signal to end the output. + + temperature: Controls the randomness or creativity of the model's responses. A higher + temperature results in more varied and less predictable text, while a lower + temperature produces more deterministic and conservative outputs. + + top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's + token generation on the most likely subset of tokens. Only tokens with + cumulative probability above this threshold are considered, controlling the + trade-off between creativity and coherence. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -127,9 +189,40 @@ def chat( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> Chat | Stream[ChatStreamingData]: """ - Create chat completion + Chat completion Args: + messages: An array of message objects that form the conversation history or context for + the model to respond to. The array must contain at least one message. + + model: Specifies the model to be used for generating responses. The chat model is + always `palmyra-x-002-32k` for conversational use. + + stream: Indicates whether the response should be streamed incrementally as it is + generated or only returned once fully complete. Streaming can be useful for + providing real-time feedback in interactive applications. + + max_tokens: Defines the maximum number of tokens (words and characters) that the model can + generate in the response. The default value is set to 16, but it can be adjusted + to allow for longer or shorter responses as needed. + + n: Specifies the number of completions (responses) to generate from the model in a + single request. This parameter allows multiple responses to be generated, + offering a variety of potential replies from which to choose. + + stop: A token or sequence of tokens that, when generated, will cause the model to stop + producing further content. This can be a single token or an array of tokens, + acting as a signal to end the output. + + temperature: Controls the randomness or creativity of the model's responses. A higher + temperature results in more varied and less predictable text, while a lower + temperature produces more deterministic and conservative outputs. + + top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's + token generation on the most likely subset of tokens. Only tokens with + cumulative probability above this threshold are considered, controlling the + trade-off between creativity and coherence. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -212,9 +305,40 @@ async def chat( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> Chat: """ - Create chat completion + Chat completion Args: + messages: An array of message objects that form the conversation history or context for + the model to respond to. The array must contain at least one message. + + model: Specifies the model to be used for generating responses. The chat model is + always `palmyra-x-002-32k` for conversational use. + + max_tokens: Defines the maximum number of tokens (words and characters) that the model can + generate in the response. The default value is set to 16, but it can be adjusted + to allow for longer or shorter responses as needed. + + n: Specifies the number of completions (responses) to generate from the model in a + single request. This parameter allows multiple responses to be generated, + offering a variety of potential replies from which to choose. + + stop: A token or sequence of tokens that, when generated, will cause the model to stop + producing further content. This can be a single token or an array of tokens, + acting as a signal to end the output. + + stream: Indicates whether the response should be streamed incrementally as it is + generated or only returned once fully complete. Streaming can be useful for + providing real-time feedback in interactive applications. + + temperature: Controls the randomness or creativity of the model's responses. A higher + temperature results in more varied and less predictable text, while a lower + temperature produces more deterministic and conservative outputs. + + top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's + token generation on the most likely subset of tokens. Only tokens with + cumulative probability above this threshold are considered, controlling the + trade-off between creativity and coherence. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -245,9 +369,40 @@ async def chat( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> AsyncStream[ChatStreamingData]: """ - Create chat completion + Chat completion Args: + messages: An array of message objects that form the conversation history or context for + the model to respond to. The array must contain at least one message. + + model: Specifies the model to be used for generating responses. The chat model is + always `palmyra-x-002-32k` for conversational use. + + stream: Indicates whether the response should be streamed incrementally as it is + generated or only returned once fully complete. Streaming can be useful for + providing real-time feedback in interactive applications. + + max_tokens: Defines the maximum number of tokens (words and characters) that the model can + generate in the response. The default value is set to 16, but it can be adjusted + to allow for longer or shorter responses as needed. + + n: Specifies the number of completions (responses) to generate from the model in a + single request. This parameter allows multiple responses to be generated, + offering a variety of potential replies from which to choose. + + stop: A token or sequence of tokens that, when generated, will cause the model to stop + producing further content. This can be a single token or an array of tokens, + acting as a signal to end the output. + + temperature: Controls the randomness or creativity of the model's responses. A higher + temperature results in more varied and less predictable text, while a lower + temperature produces more deterministic and conservative outputs. + + top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's + token generation on the most likely subset of tokens. Only tokens with + cumulative probability above this threshold are considered, controlling the + trade-off between creativity and coherence. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -278,9 +433,40 @@ async def chat( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> Chat | AsyncStream[ChatStreamingData]: """ - Create chat completion + Chat completion Args: + messages: An array of message objects that form the conversation history or context for + the model to respond to. The array must contain at least one message. + + model: Specifies the model to be used for generating responses. The chat model is + always `palmyra-x-002-32k` for conversational use. + + stream: Indicates whether the response should be streamed incrementally as it is + generated or only returned once fully complete. Streaming can be useful for + providing real-time feedback in interactive applications. + + max_tokens: Defines the maximum number of tokens (words and characters) that the model can + generate in the response. The default value is set to 16, but it can be adjusted + to allow for longer or shorter responses as needed. + + n: Specifies the number of completions (responses) to generate from the model in a + single request. This parameter allows multiple responses to be generated, + offering a variety of potential replies from which to choose. + + stop: A token or sequence of tokens that, when generated, will cause the model to stop + producing further content. This can be a single token or an array of tokens, + acting as a signal to end the output. + + temperature: Controls the randomness or creativity of the model's responses. A higher + temperature results in more varied and less predictable text, while a lower + temperature produces more deterministic and conservative outputs. + + top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's + token generation on the most likely subset of tokens. Only tokens with + cumulative probability above this threshold are considered, controlling the + trade-off between creativity and coherence. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request diff --git a/src/writerai/resources/completions.py b/src/writerai/resources/completions.py index 19c6c305..9e773db3 100644 --- a/src/writerai/resources/completions.py +++ b/src/writerai/resources/completions.py @@ -62,9 +62,36 @@ def create( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> Completion: """ - Create completion using LLM model + Text generation Args: + model: The identifier of the model to be used for processing the request. + + prompt: The input text that the model will process to generate a response. + + best_of: Specifies the number of completions to generate and return the best one. Useful + for generating multiple outputs and choosing the best based on some criteria. + + max_tokens: The maximum number of tokens that the model can generate in the response. + + random_seed: A seed used to initialize the random number generator for the model, ensuring + reproducibility of the output when the same inputs are provided. + + stop: Specifies stopping conditions for the model's output generation. This can be an + array of strings or a single string that the model will look for as a signal to + stop generating further tokens. + + stream: Determines whether the model's output should be streamed. If true, the output is + generated and sent incrementally, which can be useful for real-time + applications. + + temperature: Controls the randomness of the model's outputs. Higher values lead to more + random outputs, while lower values make the model more deterministic. + + top_p: Used to control the nucleus sampling, where only the most probable tokens with a + cumulative probability of top_p are considered for sampling, providing a way to + fine-tune the randomness of predictions. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -96,9 +123,36 @@ def create( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> Stream[StreamingData]: """ - Create completion using LLM model + Text generation Args: + model: The identifier of the model to be used for processing the request. + + prompt: The input text that the model will process to generate a response. + + stream: Determines whether the model's output should be streamed. If true, the output is + generated and sent incrementally, which can be useful for real-time + applications. + + best_of: Specifies the number of completions to generate and return the best one. Useful + for generating multiple outputs and choosing the best based on some criteria. + + max_tokens: The maximum number of tokens that the model can generate in the response. + + random_seed: A seed used to initialize the random number generator for the model, ensuring + reproducibility of the output when the same inputs are provided. + + stop: Specifies stopping conditions for the model's output generation. This can be an + array of strings or a single string that the model will look for as a signal to + stop generating further tokens. + + temperature: Controls the randomness of the model's outputs. Higher values lead to more + random outputs, while lower values make the model more deterministic. + + top_p: Used to control the nucleus sampling, where only the most probable tokens with a + cumulative probability of top_p are considered for sampling, providing a way to + fine-tune the randomness of predictions. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -130,9 +184,36 @@ def create( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> Completion | Stream[StreamingData]: """ - Create completion using LLM model + Text generation Args: + model: The identifier of the model to be used for processing the request. + + prompt: The input text that the model will process to generate a response. + + stream: Determines whether the model's output should be streamed. If true, the output is + generated and sent incrementally, which can be useful for real-time + applications. + + best_of: Specifies the number of completions to generate and return the best one. Useful + for generating multiple outputs and choosing the best based on some criteria. + + max_tokens: The maximum number of tokens that the model can generate in the response. + + random_seed: A seed used to initialize the random number generator for the model, ensuring + reproducibility of the output when the same inputs are provided. + + stop: Specifies stopping conditions for the model's output generation. This can be an + array of strings or a single string that the model will look for as a signal to + stop generating further tokens. + + temperature: Controls the randomness of the model's outputs. Higher values lead to more + random outputs, while lower values make the model more deterministic. + + top_p: Used to control the nucleus sampling, where only the most probable tokens with a + cumulative probability of top_p are considered for sampling, providing a way to + fine-tune the randomness of predictions. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -218,9 +299,36 @@ async def create( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> Completion: """ - Create completion using LLM model + Text generation Args: + model: The identifier of the model to be used for processing the request. + + prompt: The input text that the model will process to generate a response. + + best_of: Specifies the number of completions to generate and return the best one. Useful + for generating multiple outputs and choosing the best based on some criteria. + + max_tokens: The maximum number of tokens that the model can generate in the response. + + random_seed: A seed used to initialize the random number generator for the model, ensuring + reproducibility of the output when the same inputs are provided. + + stop: Specifies stopping conditions for the model's output generation. This can be an + array of strings or a single string that the model will look for as a signal to + stop generating further tokens. + + stream: Determines whether the model's output should be streamed. If true, the output is + generated and sent incrementally, which can be useful for real-time + applications. + + temperature: Controls the randomness of the model's outputs. Higher values lead to more + random outputs, while lower values make the model more deterministic. + + top_p: Used to control the nucleus sampling, where only the most probable tokens with a + cumulative probability of top_p are considered for sampling, providing a way to + fine-tune the randomness of predictions. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -252,9 +360,36 @@ async def create( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> AsyncStream[StreamingData]: """ - Create completion using LLM model + Text generation Args: + model: The identifier of the model to be used for processing the request. + + prompt: The input text that the model will process to generate a response. + + stream: Determines whether the model's output should be streamed. If true, the output is + generated and sent incrementally, which can be useful for real-time + applications. + + best_of: Specifies the number of completions to generate and return the best one. Useful + for generating multiple outputs and choosing the best based on some criteria. + + max_tokens: The maximum number of tokens that the model can generate in the response. + + random_seed: A seed used to initialize the random number generator for the model, ensuring + reproducibility of the output when the same inputs are provided. + + stop: Specifies stopping conditions for the model's output generation. This can be an + array of strings or a single string that the model will look for as a signal to + stop generating further tokens. + + temperature: Controls the randomness of the model's outputs. Higher values lead to more + random outputs, while lower values make the model more deterministic. + + top_p: Used to control the nucleus sampling, where only the most probable tokens with a + cumulative probability of top_p are considered for sampling, providing a way to + fine-tune the randomness of predictions. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -286,9 +421,36 @@ async def create( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> Completion | AsyncStream[StreamingData]: """ - Create completion using LLM model + Text generation Args: + model: The identifier of the model to be used for processing the request. + + prompt: The input text that the model will process to generate a response. + + stream: Determines whether the model's output should be streamed. If true, the output is + generated and sent incrementally, which can be useful for real-time + applications. + + best_of: Specifies the number of completions to generate and return the best one. Useful + for generating multiple outputs and choosing the best based on some criteria. + + max_tokens: The maximum number of tokens that the model can generate in the response. + + random_seed: A seed used to initialize the random number generator for the model, ensuring + reproducibility of the output when the same inputs are provided. + + stop: Specifies stopping conditions for the model's output generation. This can be an + array of strings or a single string that the model will look for as a signal to + stop generating further tokens. + + temperature: Controls the randomness of the model's outputs. Higher values lead to more + random outputs, while lower values make the model more deterministic. + + top_p: Used to control the nucleus sampling, where only the most probable tokens with a + cumulative probability of top_p are considered for sampling, providing a way to + fine-tune the randomness of predictions. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request diff --git a/src/writerai/resources/models.py b/src/writerai/resources/models.py index 906b3f61..bcf0d359 100644 --- a/src/writerai/resources/models.py +++ b/src/writerai/resources/models.py @@ -40,7 +40,7 @@ def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ModelListResponse: - """List the available models""" + """List models""" return self._get( "/v1/models", options=make_request_options( @@ -69,7 +69,7 @@ async def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ModelListResponse: - """List the available models""" + """List models""" return await self._get( "/v1/models", options=make_request_options( diff --git a/src/writerai/types/chat.py b/src/writerai/types/chat.py index a1782ea4..3ff233f8 100644 --- a/src/writerai/types/chat.py +++ b/src/writerai/types/chat.py @@ -10,21 +10,52 @@ class ChoiceMessage(BaseModel): content: str + """The text content produced by the model. + + This field contains the actual output generated, reflecting the model's response + to the input query or command. + """ role: Literal["user", "assistant", "system"] + """ + Specifies the role associated with the content, indicating whether the message + is from the 'assistant' or another defined role, helping to contextualize the + output within the interaction flow. + """ class Choice(BaseModel): finish_reason: Literal["stop", "length", "content_filter"] + """Describes the condition under which the model ceased generating content. + + Common reasons include 'length' (reached the maximum output size), 'stop' + (encountered a stop sequence), or 'content_filter' (harmful content filtered + out). + """ message: ChoiceMessage class Chat(BaseModel): id: str + """A globally unique identifier (UUID) for the response generated by the API. + + This ID can be used to reference the specific operation or transaction within + the system for tracking or debugging purposes. + """ choices: List[Choice] + """ + An array of objects representing the different outcomes or results produced by + the model based on the input provided. + """ created: int + """The Unix timestamp (in seconds) when the response was created. + + This timestamp can be used to verify the timing of the response relative to + other events or operations. + """ model: str + """Identifies the specific model used to generate the response.""" diff --git a/src/writerai/types/chat_chat_params.py b/src/writerai/types/chat_chat_params.py index 986b58d1..f257ed8e 100644 --- a/src/writerai/types/chat_chat_params.py +++ b/src/writerai/types/chat_chat_params.py @@ -10,18 +10,52 @@ class ChatChatParamsBase(TypedDict, total=False): messages: Required[Iterable[Message]] + """ + An array of message objects that form the conversation history or context for + the model to respond to. The array must contain at least one message. + """ model: Required[str] + """Specifies the model to be used for generating responses. + + The chat model is always `palmyra-x-002-32k` for conversational use. + """ max_tokens: int + """ + Defines the maximum number of tokens (words and characters) that the model can + generate in the response. The default value is set to 16, but it can be adjusted + to allow for longer or shorter responses as needed. + """ n: int + """ + Specifies the number of completions (responses) to generate from the model in a + single request. This parameter allows multiple responses to be generated, + offering a variety of potential replies from which to choose. + """ stop: Union[List[str], str] + """ + A token or sequence of tokens that, when generated, will cause the model to stop + producing further content. This can be a single token or an array of tokens, + acting as a signal to end the output. + """ temperature: float + """Controls the randomness or creativity of the model's responses. + + A higher temperature results in more varied and less predictable text, while a + lower temperature produces more deterministic and conservative outputs. + """ top_p: float + """ + Sets the threshold for "nucleus sampling," a technique to focus the model's + token generation on the most likely subset of tokens. Only tokens with + cumulative probability above this threshold are considered, controlling the + trade-off between creativity and coherence. + """ class Message(TypedDict, total=False): @@ -34,10 +68,20 @@ class Message(TypedDict, total=False): class ChatChatParamsNonStreaming(ChatChatParamsBase): stream: Literal[False] + """ + Indicates whether the response should be streamed incrementally as it is + generated or only returned once fully complete. Streaming can be useful for + providing real-time feedback in interactive applications. + """ class ChatChatParamsStreaming(ChatChatParamsBase): stream: Required[Literal[True]] + """ + Indicates whether the response should be streamed incrementally as it is + generated or only returned once fully complete. Streaming can be useful for + providing real-time feedback in interactive applications. + """ ChatChatParams = Union[ChatChatParamsNonStreaming, ChatChatParamsStreaming] diff --git a/src/writerai/types/completion.py b/src/writerai/types/completion.py index 20adc287..3631622f 100644 --- a/src/writerai/types/completion.py +++ b/src/writerai/types/completion.py @@ -9,25 +9,51 @@ class ChoiceLogProbsTopLogProb(BaseModel): additional_properties: Optional[float] = None + """For any additional_properties properties in the top_log_probs object""" class ChoiceLogProbs(BaseModel): text_offset: Optional[List[int]] = None + """ + Positional indices of each token within the original input text, useful for + analysis and mapping. + """ token_log_probs: Optional[List[float]] = None + """ + Log probabilities for each token, indicating the likelihood of each token's + occurrence. + """ tokens: Optional[List[str]] = None + """An array of tokens that comprise the generated text.""" top_log_probs: Optional[List[ChoiceLogProbsTopLogProb]] = None + """ + An array of mappings for each token to its top log probabilities, showing + detailed prediction probabilities. + """ class Choice(BaseModel): text: str + """ + The generated text output from the model, which forms the main content of the + response. + """ log_probs: Optional[ChoiceLogProbs] = None class Completion(BaseModel): choices: List[Choice] + """ + A list of choices generated by the model, each containing the text of the + completion and associated metadata such as log probabilities. + """ model: Optional[str] = None + """ + The identifier of the model that was used to generate the responses in the + 'choices' array. + """ diff --git a/src/writerai/types/completion_create_params.py b/src/writerai/types/completion_create_params.py index 60c8504d..6436f60d 100644 --- a/src/writerai/types/completion_create_params.py +++ b/src/writerai/types/completion_create_params.py @@ -10,28 +10,65 @@ class CompletionCreateParamsBase(TypedDict, total=False): model: Required[str] + """The identifier of the model to be used for processing the request.""" prompt: Required[str] + """The input text that the model will process to generate a response.""" best_of: int + """Specifies the number of completions to generate and return the best one. + + Useful for generating multiple outputs and choosing the best based on some + criteria. + """ max_tokens: int + """The maximum number of tokens that the model can generate in the response.""" random_seed: int + """ + A seed used to initialize the random number generator for the model, ensuring + reproducibility of the output when the same inputs are provided. + """ stop: Union[List[str], str] + """Specifies stopping conditions for the model's output generation. + + This can be an array of strings or a single string that the model will look for + as a signal to stop generating further tokens. + """ temperature: float + """Controls the randomness of the model's outputs. + + Higher values lead to more random outputs, while lower values make the model + more deterministic. + """ top_p: float + """ + Used to control the nucleus sampling, where only the most probable tokens with a + cumulative probability of top_p are considered for sampling, providing a way to + fine-tune the randomness of predictions. + """ class CompletionCreateParamsNonStreaming(CompletionCreateParamsBase): stream: Literal[False] + """Determines whether the model's output should be streamed. + + If true, the output is generated and sent incrementally, which can be useful for + real-time applications. + """ class CompletionCreateParamsStreaming(CompletionCreateParamsBase): stream: Required[Literal[True]] + """Determines whether the model's output should be streamed. + + If true, the output is generated and sent incrementally, which can be useful for + real-time applications. + """ CompletionCreateParams = Union[CompletionCreateParamsNonStreaming, CompletionCreateParamsStreaming] diff --git a/src/writerai/types/model_list_response.py b/src/writerai/types/model_list_response.py index 5ff56b69..db27d548 100644 --- a/src/writerai/types/model_list_response.py +++ b/src/writerai/types/model_list_response.py @@ -9,9 +9,12 @@ class Model(BaseModel): id: str + """The ID of the particular LLM that you want to use""" name: str + """The name of the particular LLM that you want to use""" class ModelListResponse(BaseModel): models: List[Model] + """The identifier of the model to be used for processing the request.""" diff --git a/tests/api_resources/test_chat.py b/tests/api_resources/test_chat.py index e6d7424d..deaa8fc4 100644 --- a/tests/api_resources/test_chat.py +++ b/tests/api_resources/test_chat.py @@ -22,11 +22,11 @@ def test_method_chat_overload_1(self, client: Writer) -> None: chat = client.chat.chat( messages=[ { - "content": "Hello!", + "content": "string", "role": "user", } ], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-002-32k", ) assert_matches_type(Chat, chat, path=["response"]) @@ -35,12 +35,12 @@ def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: chat = client.chat.chat( messages=[ { - "content": "Hello!", + "content": "string", "role": "user", "name": "string", } ], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-002-32k", max_tokens=0, n=0, stop=["string", "string", "string"], @@ -55,11 +55,11 @@ def test_raw_response_chat_overload_1(self, client: Writer) -> None: response = client.chat.with_raw_response.chat( messages=[ { - "content": "Hello!", + "content": "string", "role": "user", } ], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-002-32k", ) assert response.is_closed is True @@ -72,11 +72,11 @@ def test_streaming_response_chat_overload_1(self, client: Writer) -> None: with client.chat.with_streaming_response.chat( messages=[ { - "content": "Hello!", + "content": "string", "role": "user", } ], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-002-32k", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -91,11 +91,11 @@ def test_method_chat_overload_2(self, client: Writer) -> None: chat_stream = client.chat.chat( messages=[ { - "content": "Hello!", + "content": "string", "role": "user", } ], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-002-32k", stream=True, ) chat_stream.response.close() @@ -105,12 +105,12 @@ def test_method_chat_with_all_params_overload_2(self, client: Writer) -> None: chat_stream = client.chat.chat( messages=[ { - "content": "Hello!", + "content": "string", "role": "user", "name": "string", } ], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-002-32k", stream=True, max_tokens=0, n=0, @@ -125,11 +125,11 @@ def test_raw_response_chat_overload_2(self, client: Writer) -> None: response = client.chat.with_raw_response.chat( messages=[ { - "content": "Hello!", + "content": "string", "role": "user", } ], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-002-32k", stream=True, ) @@ -142,11 +142,11 @@ def test_streaming_response_chat_overload_2(self, client: Writer) -> None: with client.chat.with_streaming_response.chat( messages=[ { - "content": "Hello!", + "content": "string", "role": "user", } ], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-002-32k", stream=True, ) as response: assert not response.is_closed @@ -166,11 +166,11 @@ async def test_method_chat_overload_1(self, async_client: AsyncWriter) -> None: chat = await async_client.chat.chat( messages=[ { - "content": "Hello!", + "content": "string", "role": "user", } ], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-002-32k", ) assert_matches_type(Chat, chat, path=["response"]) @@ -179,12 +179,12 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW chat = await async_client.chat.chat( messages=[ { - "content": "Hello!", + "content": "string", "role": "user", "name": "string", } ], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-002-32k", max_tokens=0, n=0, stop=["string", "string", "string"], @@ -199,11 +199,11 @@ async def test_raw_response_chat_overload_1(self, async_client: AsyncWriter) -> response = await async_client.chat.with_raw_response.chat( messages=[ { - "content": "Hello!", + "content": "string", "role": "user", } ], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-002-32k", ) assert response.is_closed is True @@ -216,11 +216,11 @@ async def test_streaming_response_chat_overload_1(self, async_client: AsyncWrite async with async_client.chat.with_streaming_response.chat( messages=[ { - "content": "Hello!", + "content": "string", "role": "user", } ], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-002-32k", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -235,11 +235,11 @@ async def test_method_chat_overload_2(self, async_client: AsyncWriter) -> None: chat_stream = await async_client.chat.chat( messages=[ { - "content": "Hello!", + "content": "string", "role": "user", } ], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-002-32k", stream=True, ) await chat_stream.response.aclose() @@ -249,12 +249,12 @@ async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncW chat_stream = await async_client.chat.chat( messages=[ { - "content": "Hello!", + "content": "string", "role": "user", "name": "string", } ], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-002-32k", stream=True, max_tokens=0, n=0, @@ -269,11 +269,11 @@ async def test_raw_response_chat_overload_2(self, async_client: AsyncWriter) -> response = await async_client.chat.with_raw_response.chat( messages=[ { - "content": "Hello!", + "content": "string", "role": "user", } ], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-002-32k", stream=True, ) @@ -286,11 +286,11 @@ async def test_streaming_response_chat_overload_2(self, async_client: AsyncWrite async with async_client.chat.with_streaming_response.chat( messages=[ { - "content": "Hello!", + "content": "string", "role": "user", } ], - model="palmyra-x-chat-v2-32k", + model="palmyra-x-002-32k", stream=True, ) as response: assert not response.is_closed diff --git a/tests/api_resources/test_completions.py b/tests/api_resources/test_completions.py index d3b3a481..4db055fa 100644 --- a/tests/api_resources/test_completions.py +++ b/tests/api_resources/test_completions.py @@ -20,31 +20,31 @@ class TestCompletions: @parametrize def test_method_create_overload_1(self, client: Writer) -> None: completion = client.completions.create( - model="string", - prompt="string", + model="palmyra-x-002-instruct", + prompt="Write me an SEO article about...", ) assert_matches_type(Completion, completion, path=["response"]) @parametrize def test_method_create_with_all_params_overload_1(self, client: Writer) -> None: completion = client.completions.create( - model="string", - prompt="string", - best_of=0, - max_tokens=0, - random_seed=0, - stop=["string", "string", "string"], + model="palmyra-x-002-instruct", + prompt="Write me an SEO article about...", + best_of=1, + max_tokens=150, + random_seed=42, + stop=["."], stream=False, - temperature=0, - top_p=0, + temperature=0.7, + top_p=0.9, ) assert_matches_type(Completion, completion, path=["response"]) @parametrize def test_raw_response_create_overload_1(self, client: Writer) -> None: response = client.completions.with_raw_response.create( - model="string", - prompt="string", + model="palmyra-x-002-instruct", + prompt="Write me an SEO article about...", ) assert response.is_closed is True @@ -55,8 +55,8 @@ def test_raw_response_create_overload_1(self, client: Writer) -> None: @parametrize def test_streaming_response_create_overload_1(self, client: Writer) -> None: with client.completions.with_streaming_response.create( - model="string", - prompt="string", + model="palmyra-x-002-instruct", + prompt="Write me an SEO article about...", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -69,8 +69,8 @@ def test_streaming_response_create_overload_1(self, client: Writer) -> None: @parametrize def test_method_create_overload_2(self, client: Writer) -> None: completion_stream = client.completions.create( - model="string", - prompt="string", + model="palmyra-x-002-instruct", + prompt="Write me an SEO article about...", stream=True, ) completion_stream.response.close() @@ -78,23 +78,23 @@ def test_method_create_overload_2(self, client: Writer) -> None: @parametrize def test_method_create_with_all_params_overload_2(self, client: Writer) -> None: completion_stream = client.completions.create( - model="string", - prompt="string", + model="palmyra-x-002-instruct", + prompt="Write me an SEO article about...", stream=True, - best_of=0, - max_tokens=0, - random_seed=0, - stop=["string", "string", "string"], - temperature=0, - top_p=0, + best_of=1, + max_tokens=150, + random_seed=42, + stop=["."], + temperature=0.7, + top_p=0.9, ) completion_stream.response.close() @parametrize def test_raw_response_create_overload_2(self, client: Writer) -> None: response = client.completions.with_raw_response.create( - model="string", - prompt="string", + model="palmyra-x-002-instruct", + prompt="Write me an SEO article about...", stream=True, ) @@ -105,8 +105,8 @@ def test_raw_response_create_overload_2(self, client: Writer) -> None: @parametrize def test_streaming_response_create_overload_2(self, client: Writer) -> None: with client.completions.with_streaming_response.create( - model="string", - prompt="string", + model="palmyra-x-002-instruct", + prompt="Write me an SEO article about...", stream=True, ) as response: assert not response.is_closed @@ -124,31 +124,31 @@ class TestAsyncCompletions: @parametrize async def test_method_create_overload_1(self, async_client: AsyncWriter) -> None: completion = await async_client.completions.create( - model="string", - prompt="string", + model="palmyra-x-002-instruct", + prompt="Write me an SEO article about...", ) assert_matches_type(Completion, completion, path=["response"]) @parametrize async def test_method_create_with_all_params_overload_1(self, async_client: AsyncWriter) -> None: completion = await async_client.completions.create( - model="string", - prompt="string", - best_of=0, - max_tokens=0, - random_seed=0, - stop=["string", "string", "string"], + model="palmyra-x-002-instruct", + prompt="Write me an SEO article about...", + best_of=1, + max_tokens=150, + random_seed=42, + stop=["."], stream=False, - temperature=0, - top_p=0, + temperature=0.7, + top_p=0.9, ) assert_matches_type(Completion, completion, path=["response"]) @parametrize async def test_raw_response_create_overload_1(self, async_client: AsyncWriter) -> None: response = await async_client.completions.with_raw_response.create( - model="string", - prompt="string", + model="palmyra-x-002-instruct", + prompt="Write me an SEO article about...", ) assert response.is_closed is True @@ -159,8 +159,8 @@ async def test_raw_response_create_overload_1(self, async_client: AsyncWriter) - @parametrize async def test_streaming_response_create_overload_1(self, async_client: AsyncWriter) -> None: async with async_client.completions.with_streaming_response.create( - model="string", - prompt="string", + model="palmyra-x-002-instruct", + prompt="Write me an SEO article about...", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -173,8 +173,8 @@ async def test_streaming_response_create_overload_1(self, async_client: AsyncWri @parametrize async def test_method_create_overload_2(self, async_client: AsyncWriter) -> None: completion_stream = await async_client.completions.create( - model="string", - prompt="string", + model="palmyra-x-002-instruct", + prompt="Write me an SEO article about...", stream=True, ) await completion_stream.response.aclose() @@ -182,23 +182,23 @@ async def test_method_create_overload_2(self, async_client: AsyncWriter) -> None @parametrize async def test_method_create_with_all_params_overload_2(self, async_client: AsyncWriter) -> None: completion_stream = await async_client.completions.create( - model="string", - prompt="string", + model="palmyra-x-002-instruct", + prompt="Write me an SEO article about...", stream=True, - best_of=0, - max_tokens=0, - random_seed=0, - stop=["string", "string", "string"], - temperature=0, - top_p=0, + best_of=1, + max_tokens=150, + random_seed=42, + stop=["."], + temperature=0.7, + top_p=0.9, ) await completion_stream.response.aclose() @parametrize async def test_raw_response_create_overload_2(self, async_client: AsyncWriter) -> None: response = await async_client.completions.with_raw_response.create( - model="string", - prompt="string", + model="palmyra-x-002-instruct", + prompt="Write me an SEO article about...", stream=True, ) @@ -209,8 +209,8 @@ async def test_raw_response_create_overload_2(self, async_client: AsyncWriter) - @parametrize async def test_streaming_response_create_overload_2(self, async_client: AsyncWriter) -> None: async with async_client.completions.with_streaming_response.create( - model="string", - prompt="string", + model="palmyra-x-002-instruct", + prompt="Write me an SEO article about...", stream=True, ) as response: assert not response.is_closed diff --git a/tests/test_client.py b/tests/test_client.py index 829fd508..4d863c8b 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -724,7 +724,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> No dict( messages=[ { - "content": "Hello!", + "content": "string", "role": "user", } ], @@ -750,7 +750,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> Non dict( messages=[ { - "content": "Hello!", + "content": "string", "role": "user", } ], @@ -1455,7 +1455,7 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) dict( messages=[ { - "content": "Hello!", + "content": "string", "role": "user", } ], @@ -1481,7 +1481,7 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) dict( messages=[ { - "content": "Hello!", + "content": "string", "role": "user", } ], From 957875fe41da24641d80917b020d914c0937b2b8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 4 Jun 2024 18:09:27 +0000 Subject: [PATCH 023/399] feat(api): update via SDK Studio (#4) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f768394b..f703582d 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ It is generated with [Stainless](https://www.stainlessapi.com/). ## Documentation -The REST API documentation can be found [on dev.writer.com](https://dev.writer.com/docs/quickstart). The full API of this library can be found in [api.md](api.md). +The REST API documentation can be found [on dev.writer.com](https://dev.writer.com/api-guides/introduction). The full API of this library can be found in [api.md](api.md). ## Installation From e39f9f1506b2f76267d70220b798c3a12b55fe69 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 5 Jun 2024 06:49:23 +0000 Subject: [PATCH 024/399] chore(internal): version bump (#5) --- .release-please-manifest.json | 2 +- README.md | 2 +- bin/check-release-environment | 13 +------------ pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 5 files changed, 5 insertions(+), 16 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index aaf968a1..3d2ac0bd 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.3" + ".": "0.1.0" } \ No newline at end of file diff --git a/README.md b/README.md index f703582d..73110b89 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The REST API documentation can be found [on dev.writer.com](https://dev.writer.c ```sh # install from PyPI -pip install --pre writer-sdk +pip install writer-sdk ``` ## Usage diff --git a/bin/check-release-environment b/bin/check-release-environment index 360bcdd8..fe3283dd 100644 --- a/bin/check-release-environment +++ b/bin/check-release-environment @@ -1,20 +1,9 @@ #!/usr/bin/env bash -warnings=() errors=() if [ -z "${PYPI_TOKEN}" ]; then - warnings+=("The WRITER_PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.") -fi - -lenWarnings=${#warnings[@]} - -if [[ lenWarnings -gt 0 ]]; then - echo -e "Found the following warnings in the release environment:\n" - - for warning in "${warnings[@]}"; do - echo -e "- $warning\n" - done + errors+=("The WRITER_PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.") fi lenErrors=${#errors[@]} diff --git a/pyproject.toml b/pyproject.toml index 04c3e8e3..24ad29fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "0.1.0-alpha.3" +version = "0.1.0" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index a25555d3..200da273 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "0.1.0-alpha.3" # x-release-please-version +__version__ = "0.1.0" # x-release-please-version From 4b7ea1ee6629d81d25dab15e374712c2ec5bc736 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 5 Jun 2024 07:12:11 +0000 Subject: [PATCH 025/399] feat(api): update via SDK Studio (#7) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 73110b89..f5a02d76 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ client = Writer() stream = client.completions.create( model="palmyra-x-32k", - prompt="Hi, my name is", + prompt="Hi, my name is ", stream=True, ) for completion in stream: @@ -109,7 +109,7 @@ client = AsyncWriter() stream = await client.completions.create( model="palmyra-x-32k", - prompt="Hi, my name is", + prompt="Hi, my name is ", stream=True, ) async for completion in stream: From a110be4be077bb5639125fb6a19a99edbf462f04 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 5 Jun 2024 07:12:32 +0000 Subject: [PATCH 026/399] feat(api): update via SDK Studio (#8) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f5a02d76..73110b89 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ client = Writer() stream = client.completions.create( model="palmyra-x-32k", - prompt="Hi, my name is ", + prompt="Hi, my name is", stream=True, ) for completion in stream: @@ -109,7 +109,7 @@ client = AsyncWriter() stream = await client.completions.create( model="palmyra-x-32k", - prompt="Hi, my name is ", + prompt="Hi, my name is", stream=True, ) async for completion in stream: From ad9c1ae34a9103ca7aeb80fa949794c49ec82516 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 5 Jun 2024 07:15:46 +0000 Subject: [PATCH 027/399] chore(internal): version bump (#9) --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 3d2ac0bd..cda9cbdf 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0" + ".": "0.1.2" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 24ad29fc..9f88960b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "0.1.0" +version = "0.1.2" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index 200da273..1d37fed3 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "0.1.0" # x-release-please-version +__version__ = "0.1.2" # x-release-please-version From 4e5e10e63c65754919f09b97d8edb28bd5fee59b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 6 Jun 2024 14:53:39 +0000 Subject: [PATCH 028/399] feat(api): OpenAPI spec update via Stainless API (#10) --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 0b0545e7..f53008df 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 3 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-387e688cfbf5098041d47c9c918c15d4978f98768b4daf901267aea8affc0a30.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-1bf320b94d765777875e8999e58068f684f81c4657bc25bf0d321b6adce778e1.yml From 1ea16b448c46545292a754ee6819f503282dfabe Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 28 Jun 2024 17:49:23 +0000 Subject: [PATCH 029/399] feat(api): add support for graphs and files endpoints (#15) --- .gitignore | 1 + .stats.yml | 4 +- README.md | 2 +- api.md | 40 ++ bin/publish-pypi | 3 + pyproject.toml | 15 + requirements-dev.lock | 3 +- requirements.lock | 3 +- src/writerai/_base_client.py | 25 +- src/writerai/_client.py | 16 + src/writerai/_utils/__init__.py | 4 + src/writerai/_utils/_reflection.py | 42 ++ src/writerai/_utils/_sync.py | 19 +- src/writerai/pagination.py | 85 +++ src/writerai/resources/__init__.py | 28 + src/writerai/resources/files.py | 527 ++++++++++++++ src/writerai/resources/graphs.py | 672 ++++++++++++++++++ src/writerai/types/__init__.py | 13 + src/writerai/types/file.py | 18 + src/writerai/types/file_delete_response.py | 13 + src/writerai/types/file_list_params.py | 19 + src/writerai/types/file_upload_params.py | 20 + src/writerai/types/graph.py | 30 + .../types/graph_add_file_to_graph_params.py | 11 + src/writerai/types/graph_create_params.py | 13 + src/writerai/types/graph_create_response.py | 18 + src/writerai/types/graph_delete_response.py | 13 + src/writerai/types/graph_list_params.py | 17 + .../graph_remove_file_from_graph_response.py | 13 + src/writerai/types/graph_update_params.py | 13 + src/writerai/types/graph_update_response.py | 18 + src/writerai/types/model_list_response.py | 2 +- tests/api_resources/test_files.py | 449 ++++++++++++ tests/api_resources/test_graphs.py | 612 ++++++++++++++++ 34 files changed, 2769 insertions(+), 12 deletions(-) create mode 100644 src/writerai/_utils/_reflection.py create mode 100644 src/writerai/pagination.py create mode 100644 src/writerai/resources/files.py create mode 100644 src/writerai/resources/graphs.py create mode 100644 src/writerai/types/file.py create mode 100644 src/writerai/types/file_delete_response.py create mode 100644 src/writerai/types/file_list_params.py create mode 100644 src/writerai/types/file_upload_params.py create mode 100644 src/writerai/types/graph.py create mode 100644 src/writerai/types/graph_add_file_to_graph_params.py create mode 100644 src/writerai/types/graph_create_params.py create mode 100644 src/writerai/types/graph_create_response.py create mode 100644 src/writerai/types/graph_delete_response.py create mode 100644 src/writerai/types/graph_list_params.py create mode 100644 src/writerai/types/graph_remove_file_from_graph_response.py create mode 100644 src/writerai/types/graph_update_params.py create mode 100644 src/writerai/types/graph_update_response.py create mode 100644 tests/api_resources/test_files.py create mode 100644 tests/api_resources/test_graphs.py diff --git a/.gitignore b/.gitignore index 0f9a66a9..87797408 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.prism.log .vscode _dev diff --git a/.stats.yml b/.stats.yml index f53008df..e396c3c5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ -configured_endpoints: 3 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-1bf320b94d765777875e8999e58068f684f81c4657bc25bf0d321b6adce778e1.yml +configured_endpoints: 15 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-e54b835d1d64d37343017f4919c60998496a1b2b5c8e0c67af82093efc613f2f.yml diff --git a/README.md b/README.md index 73110b89..03ab73fe 100644 --- a/README.md +++ b/README.md @@ -352,7 +352,7 @@ You can directly override the [httpx client](https://www.python-httpx.org/api/#c - Support for proxies - Custom transports -- Additional [advanced](https://www.python-httpx.org/advanced/#client-instances) functionality +- Additional [advanced](https://www.python-httpx.org/advanced/clients/) functionality ```python from writerai import Writer, DefaultHttpxClient diff --git a/api.md b/api.md index 0c42b4dc..9a27ad94 100644 --- a/api.md +++ b/api.md @@ -33,3 +33,43 @@ from writerai.types import ModelListResponse Methods: - client.models.list() -> ModelListResponse + +# Graphs + +Types: + +```python +from writerai.types import ( + Graph, + GraphCreateResponse, + GraphUpdateResponse, + GraphDeleteResponse, + GraphRemoveFileFromGraphResponse, +) +``` + +Methods: + +- client.graphs.create(\*\*params) -> GraphCreateResponse +- client.graphs.retrieve(graph_id) -> Graph +- client.graphs.update(graph_id, \*\*params) -> GraphUpdateResponse +- client.graphs.list(\*\*params) -> SyncCursorPage[Graph] +- client.graphs.delete(graph_id) -> GraphDeleteResponse +- client.graphs.add_file_to_graph(graph_id, \*\*params) -> File +- client.graphs.remove_file_from_graph(file_id, \*, graph_id) -> GraphRemoveFileFromGraphResponse + +# Files + +Types: + +```python +from writerai.types import File, FileDeleteResponse +``` + +Methods: + +- client.files.retrieve(file_id) -> File +- client.files.list(\*\*params) -> SyncCursorPage[File] +- client.files.delete(file_id) -> FileDeleteResponse +- client.files.download(file_id) -> BinaryAPIResponse +- client.files.upload(\*\*params) -> File diff --git a/bin/publish-pypi b/bin/publish-pypi index 826054e9..05bfccbb 100644 --- a/bin/publish-pypi +++ b/bin/publish-pypi @@ -3,4 +3,7 @@ set -eux mkdir -p dist rye build --clean +# Patching importlib-metadata version until upstream library version is updated +# https://github.com/pypa/twine/issues/977#issuecomment-2189800841 +"$HOME/.rye/self/bin/python3" -m pip install 'importlib-metadata==7.2.1' rye publish --yes --token=$PYPI_TOKEN diff --git a/pyproject.toml b/pyproject.toml index 9f88960b..6d2d20a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,6 +99,21 @@ include = [ [tool.hatch.build.targets.wheel] packages = ["src/writerai"] +[tool.hatch.build.targets.sdist] +# Basically everything except hidden files/directories (such as .github, .devcontainers, .python-version, etc) +include = [ + "/*.toml", + "/*.json", + "/*.lock", + "/*.md", + "/mypy.ini", + "/noxfile.py", + "bin/*", + "examples/*", + "src/*", + "tests/*", +] + [tool.hatch.metadata.hooks.fancy-pypi-readme] content-type = "text/markdown" diff --git a/requirements-dev.lock b/requirements-dev.lock index 1b60f5e1..70a53ed3 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -10,7 +10,7 @@ -e file:. annotated-types==0.6.0 # via pydantic -anyio==4.1.0 +anyio==4.4.0 # via httpx # via writer-sdk argcomplete==3.1.2 @@ -86,6 +86,7 @@ tomli==2.0.1 # via mypy # via pytest typing-extensions==4.8.0 + # via anyio # via mypy # via pydantic # via pydantic-core diff --git a/requirements.lock b/requirements.lock index efb1c1b4..bd53c624 100644 --- a/requirements.lock +++ b/requirements.lock @@ -10,7 +10,7 @@ -e file:. annotated-types==0.6.0 # via pydantic -anyio==4.1.0 +anyio==4.4.0 # via httpx # via writer-sdk certifi==2023.7.22 @@ -38,6 +38,7 @@ sniffio==1.3.0 # via httpx # via writer-sdk typing-extensions==4.8.0 + # via anyio # via pydantic # via pydantic-core # via writer-sdk diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index 633afbb6..5665fda9 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -60,7 +60,7 @@ RequestOptions, ModelBuilderProtocol, ) -from ._utils import is_dict, is_list, is_given, lru_cache, is_mapping +from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping from ._compat import model_copy, model_dump from ._models import GenericModel, FinalRequestOptions, validate_type, construct_type from ._response import ( @@ -358,6 +358,7 @@ def __init__( self._custom_query = custom_query or {} self._strict_response_validation = _strict_response_validation self._idempotency_header = None + self._platform: Platform | None = None if max_retries is None: # pyright: ignore[reportUnnecessaryComparison] raise TypeError( @@ -456,7 +457,7 @@ def _build_request( raise RuntimeError(f"Unexpected JSON data type, {type(json_data)}, cannot merge with `extra_body`") headers = self._build_headers(options) - params = _merge_mappings(self._custom_query, options.params) + params = _merge_mappings(self.default_query, options.params) content_type = headers.get("Content-Type") # If the given Content-Type header is multipart/form-data then it @@ -592,6 +593,12 @@ def default_headers(self) -> dict[str, str | Omit]: **self._custom_headers, } + @property + def default_query(self) -> dict[str, object]: + return { + **self._custom_query, + } + def _validate_headers( self, headers: Headers, # noqa: ARG002 @@ -616,7 +623,10 @@ def base_url(self, url: URL | str) -> None: self._base_url = self._enforce_trailing_slash(url if isinstance(url, URL) else URL(url)) def platform_headers(self) -> Dict[str, str]: - return platform_headers(self._version) + # the actual implementation is in a separate `lru_cache` decorated + # function because adding `lru_cache` to methods will leak memory + # https://github.com/python/cpython/issues/88476 + return platform_headers(self._version, platform=self._platform) def _parse_retry_after_header(self, response_headers: Optional[httpx.Headers] = None) -> float | None: """Returns a float of the number of seconds (not milliseconds) to wait after retrying, or None if unspecified. @@ -1492,6 +1502,11 @@ async def _request( stream_cls: type[_AsyncStreamT] | None, remaining_retries: int | None, ) -> ResponseT | _AsyncStreamT: + if self._platform is None: + # `get_platform` can make blocking IO calls so we + # execute it earlier while we are in an async context + self._platform = await asyncify(get_platform)() + cast_to = self._maybe_override_cast_to(cast_to, options) await self._prepare_options(options) @@ -1915,11 +1930,11 @@ def get_platform() -> Platform: @lru_cache(maxsize=None) -def platform_headers(version: str) -> Dict[str, str]: +def platform_headers(version: str, *, platform: Platform | None) -> Dict[str, str]: return { "X-Stainless-Lang": "python", "X-Stainless-Package-Version": version, - "X-Stainless-OS": str(get_platform()), + "X-Stainless-OS": str(platform or get_platform()), "X-Stainless-Arch": str(get_architecture()), "X-Stainless-Runtime": get_python_runtime(), "X-Stainless-Runtime-Version": get_python_version(), diff --git a/src/writerai/_client.py b/src/writerai/_client.py index 97dc25bc..ab243071 100644 --- a/src/writerai/_client.py +++ b/src/writerai/_client.py @@ -49,6 +49,8 @@ class Writer(SyncAPIClient): chat: resources.ChatResource completions: resources.CompletionsResource models: resources.ModelsResource + graphs: resources.GraphsResource + files: resources.FilesResource with_raw_response: WriterWithRawResponse with_streaming_response: WriterWithStreamedResponse @@ -111,6 +113,8 @@ def __init__( self.chat = resources.ChatResource(self) self.completions = resources.CompletionsResource(self) self.models = resources.ModelsResource(self) + self.graphs = resources.GraphsResource(self) + self.files = resources.FilesResource(self) self.with_raw_response = WriterWithRawResponse(self) self.with_streaming_response = WriterWithStreamedResponse(self) @@ -223,6 +227,8 @@ class AsyncWriter(AsyncAPIClient): chat: resources.AsyncChatResource completions: resources.AsyncCompletionsResource models: resources.AsyncModelsResource + graphs: resources.AsyncGraphsResource + files: resources.AsyncFilesResource with_raw_response: AsyncWriterWithRawResponse with_streaming_response: AsyncWriterWithStreamedResponse @@ -285,6 +291,8 @@ def __init__( self.chat = resources.AsyncChatResource(self) self.completions = resources.AsyncCompletionsResource(self) self.models = resources.AsyncModelsResource(self) + self.graphs = resources.AsyncGraphsResource(self) + self.files = resources.AsyncFilesResource(self) self.with_raw_response = AsyncWriterWithRawResponse(self) self.with_streaming_response = AsyncWriterWithStreamedResponse(self) @@ -398,6 +406,8 @@ def __init__(self, client: Writer) -> None: self.chat = resources.ChatResourceWithRawResponse(client.chat) self.completions = resources.CompletionsResourceWithRawResponse(client.completions) self.models = resources.ModelsResourceWithRawResponse(client.models) + self.graphs = resources.GraphsResourceWithRawResponse(client.graphs) + self.files = resources.FilesResourceWithRawResponse(client.files) class AsyncWriterWithRawResponse: @@ -405,6 +415,8 @@ def __init__(self, client: AsyncWriter) -> None: self.chat = resources.AsyncChatResourceWithRawResponse(client.chat) self.completions = resources.AsyncCompletionsResourceWithRawResponse(client.completions) self.models = resources.AsyncModelsResourceWithRawResponse(client.models) + self.graphs = resources.AsyncGraphsResourceWithRawResponse(client.graphs) + self.files = resources.AsyncFilesResourceWithRawResponse(client.files) class WriterWithStreamedResponse: @@ -412,6 +424,8 @@ def __init__(self, client: Writer) -> None: self.chat = resources.ChatResourceWithStreamingResponse(client.chat) self.completions = resources.CompletionsResourceWithStreamingResponse(client.completions) self.models = resources.ModelsResourceWithStreamingResponse(client.models) + self.graphs = resources.GraphsResourceWithStreamingResponse(client.graphs) + self.files = resources.FilesResourceWithStreamingResponse(client.files) class AsyncWriterWithStreamedResponse: @@ -419,6 +433,8 @@ def __init__(self, client: AsyncWriter) -> None: self.chat = resources.AsyncChatResourceWithStreamingResponse(client.chat) self.completions = resources.AsyncCompletionsResourceWithStreamingResponse(client.completions) self.models = resources.AsyncModelsResourceWithStreamingResponse(client.models) + self.graphs = resources.AsyncGraphsResourceWithStreamingResponse(client.graphs) + self.files = resources.AsyncFilesResourceWithStreamingResponse(client.files) Client = Writer diff --git a/src/writerai/_utils/__init__.py b/src/writerai/_utils/__init__.py index 31b5b227..3efe66c8 100644 --- a/src/writerai/_utils/__init__.py +++ b/src/writerai/_utils/__init__.py @@ -49,3 +49,7 @@ maybe_transform as maybe_transform, async_maybe_transform as async_maybe_transform, ) +from ._reflection import ( + function_has_argument as function_has_argument, + assert_signatures_in_sync as assert_signatures_in_sync, +) diff --git a/src/writerai/_utils/_reflection.py b/src/writerai/_utils/_reflection.py new file mode 100644 index 00000000..9a53c7bd --- /dev/null +++ b/src/writerai/_utils/_reflection.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import inspect +from typing import Any, Callable + + +def function_has_argument(func: Callable[..., Any], arg_name: str) -> bool: + """Returns whether or not the given function has a specific parameter""" + sig = inspect.signature(func) + return arg_name in sig.parameters + + +def assert_signatures_in_sync( + source_func: Callable[..., Any], + check_func: Callable[..., Any], + *, + exclude_params: set[str] = set(), +) -> None: + """Ensure that the signature of the second function matches the first.""" + + check_sig = inspect.signature(check_func) + source_sig = inspect.signature(source_func) + + errors: list[str] = [] + + for name, source_param in source_sig.parameters.items(): + if name in exclude_params: + continue + + custom_param = check_sig.parameters.get(name) + if not custom_param: + errors.append(f"the `{name}` param is missing") + continue + + if custom_param.annotation != source_param.annotation: + errors.append( + f"types for the `{name}` param are do not match; source={repr(source_param.annotation)} checking={repr(source_param.annotation)}" + ) + continue + + if errors: + raise AssertionError(f"{len(errors)} errors encountered when comparing signatures:\n\n" + "\n\n".join(errors)) diff --git a/src/writerai/_utils/_sync.py b/src/writerai/_utils/_sync.py index 595924e5..d0d81033 100644 --- a/src/writerai/_utils/_sync.py +++ b/src/writerai/_utils/_sync.py @@ -7,6 +7,8 @@ import anyio import anyio.to_thread +from ._reflection import function_has_argument + T_Retval = TypeVar("T_Retval") T_ParamSpec = ParamSpec("T_ParamSpec") @@ -59,6 +61,21 @@ def do_work(arg1, arg2, kwarg1="", kwarg2="") -> str: async def wrapper(*args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs) -> T_Retval: partial_f = functools.partial(function, *args, **kwargs) - return await anyio.to_thread.run_sync(partial_f, cancellable=cancellable, limiter=limiter) + + # In `v4.1.0` anyio added the `abandon_on_cancel` argument and deprecated the old + # `cancellable` argument, so we need to use the new `abandon_on_cancel` to avoid + # surfacing deprecation warnings. + if function_has_argument(anyio.to_thread.run_sync, "abandon_on_cancel"): + return await anyio.to_thread.run_sync( + partial_f, + abandon_on_cancel=cancellable, + limiter=limiter, + ) + + return await anyio.to_thread.run_sync( + partial_f, + cancellable=cancellable, + limiter=limiter, + ) return wrapper diff --git a/src/writerai/pagination.py b/src/writerai/pagination.py new file mode 100644 index 00000000..16ba6875 --- /dev/null +++ b/src/writerai/pagination.py @@ -0,0 +1,85 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Any, List, Generic, TypeVar, Optional, cast +from typing_extensions import Protocol, override, runtime_checkable + +from ._base_client import BasePage, PageInfo, BaseSyncPage, BaseAsyncPage + +__all__ = ["SyncCursorPage", "AsyncCursorPage"] + +_T = TypeVar("_T") + + +@runtime_checkable +class CursorPageItem(Protocol): + id: Optional[str] + + +class SyncCursorPage(BaseSyncPage[_T], BasePage[_T], Generic[_T]): + data: List[_T] + has_more: bool + + @override + def _get_page_items(self) -> List[_T]: + data = self.data + if not data: + return [] + return data + + @override + def next_page_info(self) -> Optional[PageInfo]: + is_forwards = not self._options.params.get("before", False) + + data = self.data + if not data: + return None + + if is_forwards: + item = cast(Any, data[-1]) + if not isinstance(item, CursorPageItem) or item.id is None: + # TODO emit warning log + return None + + return PageInfo(params={"after": item.id}) + else: + item = cast(Any, self.data[0]) + if not isinstance(item, CursorPageItem) or item.id is None: + # TODO emit warning log + return None + + return PageInfo(params={"before": item.id}) + + +class AsyncCursorPage(BaseAsyncPage[_T], BasePage[_T], Generic[_T]): + data: List[_T] + has_more: bool + + @override + def _get_page_items(self) -> List[_T]: + data = self.data + if not data: + return [] + return data + + @override + def next_page_info(self) -> Optional[PageInfo]: + is_forwards = not self._options.params.get("before", False) + + data = self.data + if not data: + return None + + if is_forwards: + item = cast(Any, data[-1]) + if not isinstance(item, CursorPageItem) or item.id is None: + # TODO emit warning log + return None + + return PageInfo(params={"after": item.id}) + else: + item = cast(Any, self.data[0]) + if not isinstance(item, CursorPageItem) or item.id is None: + # TODO emit warning log + return None + + return PageInfo(params={"before": item.id}) diff --git a/src/writerai/resources/__init__.py b/src/writerai/resources/__init__.py index d4fe7058..4d17d607 100644 --- a/src/writerai/resources/__init__.py +++ b/src/writerai/resources/__init__.py @@ -8,6 +8,22 @@ ChatResourceWithStreamingResponse, AsyncChatResourceWithStreamingResponse, ) +from .files import ( + FilesResource, + AsyncFilesResource, + FilesResourceWithRawResponse, + AsyncFilesResourceWithRawResponse, + FilesResourceWithStreamingResponse, + AsyncFilesResourceWithStreamingResponse, +) +from .graphs import ( + GraphsResource, + AsyncGraphsResource, + GraphsResourceWithRawResponse, + AsyncGraphsResourceWithRawResponse, + GraphsResourceWithStreamingResponse, + AsyncGraphsResourceWithStreamingResponse, +) from .models import ( ModelsResource, AsyncModelsResource, @@ -44,4 +60,16 @@ "AsyncModelsResourceWithRawResponse", "ModelsResourceWithStreamingResponse", "AsyncModelsResourceWithStreamingResponse", + "GraphsResource", + "AsyncGraphsResource", + "GraphsResourceWithRawResponse", + "AsyncGraphsResourceWithRawResponse", + "GraphsResourceWithStreamingResponse", + "AsyncGraphsResourceWithStreamingResponse", + "FilesResource", + "AsyncFilesResource", + "FilesResourceWithRawResponse", + "AsyncFilesResourceWithRawResponse", + "FilesResourceWithStreamingResponse", + "AsyncFilesResourceWithStreamingResponse", ] diff --git a/src/writerai/resources/files.py b/src/writerai/resources/files.py new file mode 100644 index 00000000..64fc8428 --- /dev/null +++ b/src/writerai/resources/files.py @@ -0,0 +1,527 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal + +import httpx + +from ..types import file_list_params, file_upload_params +from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes +from .._utils import ( + maybe_transform, + async_maybe_transform, +) +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + BinaryAPIResponse, + AsyncBinaryAPIResponse, + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + to_custom_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_custom_streamed_response_wrapper, + async_to_custom_raw_response_wrapper, + async_to_custom_streamed_response_wrapper, +) +from ..pagination import SyncCursorPage, AsyncCursorPage +from ..types.file import File +from .._base_client import ( + AsyncPaginator, + make_request_options, +) +from ..types.file_delete_response import FileDeleteResponse + +__all__ = ["FilesResource", "AsyncFilesResource"] + + +class FilesResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> FilesResourceWithRawResponse: + return FilesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> FilesResourceWithStreamingResponse: + return FilesResourceWithStreamingResponse(self) + + def retrieve( + self, + file_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> File: + """ + Get metadata of a file + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + return self._get( + f"/v1/files/{file_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=File, + ) + + def list( + self, + *, + after: str | NotGiven = NOT_GIVEN, + before: str | NotGiven = NOT_GIVEN, + graph_id: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncCursorPage[File]: + """ + Get metadata of all files + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/files", + page=SyncCursorPage[File], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "after": after, + "before": before, + "graph_id": graph_id, + "limit": limit, + "order": order, + }, + file_list_params.FileListParams, + ), + ), + model=File, + ) + + def delete( + self, + file_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> FileDeleteResponse: + """ + Delete file + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + return self._delete( + f"/v1/files/{file_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=FileDeleteResponse, + ) + + def download( + self, + file_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BinaryAPIResponse: + """ + Download a file + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} + return self._get( + f"/v1/files/{file_id}/download", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BinaryAPIResponse, + ) + + def upload( + self, + *, + content: FileTypes, + content_disposition: str, + content_length: int, + content_type: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> File: + """ + Upload file + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = { + "Content-Disposition": content_disposition, + "Content-Length": str(content_length), + "Content-Type": content_type, + **(extra_headers or {}), + } + return self._post( + "/v1/files", + body=maybe_transform(content, file_upload_params.FileUploadParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=File, + ) + + +class AsyncFilesResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncFilesResourceWithRawResponse: + return AsyncFilesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncFilesResourceWithStreamingResponse: + return AsyncFilesResourceWithStreamingResponse(self) + + async def retrieve( + self, + file_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> File: + """ + Get metadata of a file + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + return await self._get( + f"/v1/files/{file_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=File, + ) + + def list( + self, + *, + after: str | NotGiven = NOT_GIVEN, + before: str | NotGiven = NOT_GIVEN, + graph_id: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[File, AsyncCursorPage[File]]: + """ + Get metadata of all files + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/files", + page=AsyncCursorPage[File], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "after": after, + "before": before, + "graph_id": graph_id, + "limit": limit, + "order": order, + }, + file_list_params.FileListParams, + ), + ), + model=File, + ) + + async def delete( + self, + file_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> FileDeleteResponse: + """ + Delete file + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + return await self._delete( + f"/v1/files/{file_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=FileDeleteResponse, + ) + + async def download( + self, + file_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncBinaryAPIResponse: + """ + Download a file + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} + return await self._get( + f"/v1/files/{file_id}/download", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AsyncBinaryAPIResponse, + ) + + async def upload( + self, + *, + content: FileTypes, + content_disposition: str, + content_length: int, + content_type: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> File: + """ + Upload file + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = { + "Content-Disposition": content_disposition, + "Content-Length": str(content_length), + "Content-Type": content_type, + **(extra_headers or {}), + } + return await self._post( + "/v1/files", + body=await async_maybe_transform(content, file_upload_params.FileUploadParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=File, + ) + + +class FilesResourceWithRawResponse: + def __init__(self, files: FilesResource) -> None: + self._files = files + + self.retrieve = to_raw_response_wrapper( + files.retrieve, + ) + self.list = to_raw_response_wrapper( + files.list, + ) + self.delete = to_raw_response_wrapper( + files.delete, + ) + self.download = to_custom_raw_response_wrapper( + files.download, + BinaryAPIResponse, + ) + self.upload = to_raw_response_wrapper( + files.upload, + ) + + +class AsyncFilesResourceWithRawResponse: + def __init__(self, files: AsyncFilesResource) -> None: + self._files = files + + self.retrieve = async_to_raw_response_wrapper( + files.retrieve, + ) + self.list = async_to_raw_response_wrapper( + files.list, + ) + self.delete = async_to_raw_response_wrapper( + files.delete, + ) + self.download = async_to_custom_raw_response_wrapper( + files.download, + AsyncBinaryAPIResponse, + ) + self.upload = async_to_raw_response_wrapper( + files.upload, + ) + + +class FilesResourceWithStreamingResponse: + def __init__(self, files: FilesResource) -> None: + self._files = files + + self.retrieve = to_streamed_response_wrapper( + files.retrieve, + ) + self.list = to_streamed_response_wrapper( + files.list, + ) + self.delete = to_streamed_response_wrapper( + files.delete, + ) + self.download = to_custom_streamed_response_wrapper( + files.download, + StreamedBinaryAPIResponse, + ) + self.upload = to_streamed_response_wrapper( + files.upload, + ) + + +class AsyncFilesResourceWithStreamingResponse: + def __init__(self, files: AsyncFilesResource) -> None: + self._files = files + + self.retrieve = async_to_streamed_response_wrapper( + files.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + files.list, + ) + self.delete = async_to_streamed_response_wrapper( + files.delete, + ) + self.download = async_to_custom_streamed_response_wrapper( + files.download, + AsyncStreamedBinaryAPIResponse, + ) + self.upload = async_to_streamed_response_wrapper( + files.upload, + ) diff --git a/src/writerai/resources/graphs.py b/src/writerai/resources/graphs.py new file mode 100644 index 00000000..73c7add7 --- /dev/null +++ b/src/writerai/resources/graphs.py @@ -0,0 +1,672 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal + +import httpx + +from ..types import ( + graph_list_params, + graph_create_params, + graph_update_params, + graph_add_file_to_graph_params, +) +from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from .._utils import ( + maybe_transform, + async_maybe_transform, +) +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..pagination import SyncCursorPage, AsyncCursorPage +from ..types.file import File +from ..types.graph import Graph +from .._base_client import ( + AsyncPaginator, + make_request_options, +) +from ..types.graph_create_response import GraphCreateResponse +from ..types.graph_delete_response import GraphDeleteResponse +from ..types.graph_update_response import GraphUpdateResponse +from ..types.graph_remove_file_from_graph_response import GraphRemoveFileFromGraphResponse + +__all__ = ["GraphsResource", "AsyncGraphsResource"] + + +class GraphsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> GraphsResourceWithRawResponse: + return GraphsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> GraphsResourceWithStreamingResponse: + return GraphsResourceWithStreamingResponse(self) + + def create( + self, + *, + name: str, + description: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> GraphCreateResponse: + """ + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/v1/graphs", + body=maybe_transform( + { + "name": name, + "description": description, + }, + graph_create_params.GraphCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GraphCreateResponse, + ) + + def retrieve( + self, + graph_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Graph: + """ + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not graph_id: + raise ValueError(f"Expected a non-empty value for `graph_id` but received {graph_id!r}") + return self._get( + f"/v1/graphs/{graph_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Graph, + ) + + def update( + self, + graph_id: str, + *, + name: str, + description: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> GraphUpdateResponse: + """ + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not graph_id: + raise ValueError(f"Expected a non-empty value for `graph_id` but received {graph_id!r}") + return self._put( + f"/v1/graphs/{graph_id}", + body=maybe_transform( + { + "name": name, + "description": description, + }, + graph_update_params.GraphUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GraphUpdateResponse, + ) + + def list( + self, + *, + after: str | NotGiven = NOT_GIVEN, + before: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncCursorPage[Graph]: + """ + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/graphs", + page=SyncCursorPage[Graph], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "after": after, + "before": before, + "limit": limit, + "order": order, + }, + graph_list_params.GraphListParams, + ), + ), + model=Graph, + ) + + def delete( + self, + graph_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> GraphDeleteResponse: + """ + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not graph_id: + raise ValueError(f"Expected a non-empty value for `graph_id` but received {graph_id!r}") + return self._delete( + f"/v1/graphs/{graph_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GraphDeleteResponse, + ) + + def add_file_to_graph( + self, + graph_id: str, + *, + file_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> File: + """ + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not graph_id: + raise ValueError(f"Expected a non-empty value for `graph_id` but received {graph_id!r}") + return self._post( + f"/v1/graphs/{graph_id}/file", + body=maybe_transform({"file_id": file_id}, graph_add_file_to_graph_params.GraphAddFileToGraphParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=File, + ) + + def remove_file_from_graph( + self, + file_id: str, + *, + graph_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> GraphRemoveFileFromGraphResponse: + """ + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not graph_id: + raise ValueError(f"Expected a non-empty value for `graph_id` but received {graph_id!r}") + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + return self._delete( + f"/v1/graphs/{graph_id}/file/{file_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GraphRemoveFileFromGraphResponse, + ) + + +class AsyncGraphsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncGraphsResourceWithRawResponse: + return AsyncGraphsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncGraphsResourceWithStreamingResponse: + return AsyncGraphsResourceWithStreamingResponse(self) + + async def create( + self, + *, + name: str, + description: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> GraphCreateResponse: + """ + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/v1/graphs", + body=await async_maybe_transform( + { + "name": name, + "description": description, + }, + graph_create_params.GraphCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GraphCreateResponse, + ) + + async def retrieve( + self, + graph_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Graph: + """ + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not graph_id: + raise ValueError(f"Expected a non-empty value for `graph_id` but received {graph_id!r}") + return await self._get( + f"/v1/graphs/{graph_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Graph, + ) + + async def update( + self, + graph_id: str, + *, + name: str, + description: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> GraphUpdateResponse: + """ + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not graph_id: + raise ValueError(f"Expected a non-empty value for `graph_id` but received {graph_id!r}") + return await self._put( + f"/v1/graphs/{graph_id}", + body=await async_maybe_transform( + { + "name": name, + "description": description, + }, + graph_update_params.GraphUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GraphUpdateResponse, + ) + + def list( + self, + *, + after: str | NotGiven = NOT_GIVEN, + before: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[Graph, AsyncCursorPage[Graph]]: + """ + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/graphs", + page=AsyncCursorPage[Graph], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "after": after, + "before": before, + "limit": limit, + "order": order, + }, + graph_list_params.GraphListParams, + ), + ), + model=Graph, + ) + + async def delete( + self, + graph_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> GraphDeleteResponse: + """ + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not graph_id: + raise ValueError(f"Expected a non-empty value for `graph_id` but received {graph_id!r}") + return await self._delete( + f"/v1/graphs/{graph_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GraphDeleteResponse, + ) + + async def add_file_to_graph( + self, + graph_id: str, + *, + file_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> File: + """ + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not graph_id: + raise ValueError(f"Expected a non-empty value for `graph_id` but received {graph_id!r}") + return await self._post( + f"/v1/graphs/{graph_id}/file", + body=await async_maybe_transform( + {"file_id": file_id}, graph_add_file_to_graph_params.GraphAddFileToGraphParams + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=File, + ) + + async def remove_file_from_graph( + self, + file_id: str, + *, + graph_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> GraphRemoveFileFromGraphResponse: + """ + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not graph_id: + raise ValueError(f"Expected a non-empty value for `graph_id` but received {graph_id!r}") + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + return await self._delete( + f"/v1/graphs/{graph_id}/file/{file_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GraphRemoveFileFromGraphResponse, + ) + + +class GraphsResourceWithRawResponse: + def __init__(self, graphs: GraphsResource) -> None: + self._graphs = graphs + + self.create = to_raw_response_wrapper( + graphs.create, + ) + self.retrieve = to_raw_response_wrapper( + graphs.retrieve, + ) + self.update = to_raw_response_wrapper( + graphs.update, + ) + self.list = to_raw_response_wrapper( + graphs.list, + ) + self.delete = to_raw_response_wrapper( + graphs.delete, + ) + self.add_file_to_graph = to_raw_response_wrapper( + graphs.add_file_to_graph, + ) + self.remove_file_from_graph = to_raw_response_wrapper( + graphs.remove_file_from_graph, + ) + + +class AsyncGraphsResourceWithRawResponse: + def __init__(self, graphs: AsyncGraphsResource) -> None: + self._graphs = graphs + + self.create = async_to_raw_response_wrapper( + graphs.create, + ) + self.retrieve = async_to_raw_response_wrapper( + graphs.retrieve, + ) + self.update = async_to_raw_response_wrapper( + graphs.update, + ) + self.list = async_to_raw_response_wrapper( + graphs.list, + ) + self.delete = async_to_raw_response_wrapper( + graphs.delete, + ) + self.add_file_to_graph = async_to_raw_response_wrapper( + graphs.add_file_to_graph, + ) + self.remove_file_from_graph = async_to_raw_response_wrapper( + graphs.remove_file_from_graph, + ) + + +class GraphsResourceWithStreamingResponse: + def __init__(self, graphs: GraphsResource) -> None: + self._graphs = graphs + + self.create = to_streamed_response_wrapper( + graphs.create, + ) + self.retrieve = to_streamed_response_wrapper( + graphs.retrieve, + ) + self.update = to_streamed_response_wrapper( + graphs.update, + ) + self.list = to_streamed_response_wrapper( + graphs.list, + ) + self.delete = to_streamed_response_wrapper( + graphs.delete, + ) + self.add_file_to_graph = to_streamed_response_wrapper( + graphs.add_file_to_graph, + ) + self.remove_file_from_graph = to_streamed_response_wrapper( + graphs.remove_file_from_graph, + ) + + +class AsyncGraphsResourceWithStreamingResponse: + def __init__(self, graphs: AsyncGraphsResource) -> None: + self._graphs = graphs + + self.create = async_to_streamed_response_wrapper( + graphs.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + graphs.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + graphs.update, + ) + self.list = async_to_streamed_response_wrapper( + graphs.list, + ) + self.delete = async_to_streamed_response_wrapper( + graphs.delete, + ) + self.add_file_to_graph = async_to_streamed_response_wrapper( + graphs.add_file_to_graph, + ) + self.remove_file_from_graph = async_to_streamed_response_wrapper( + graphs.remove_file_from_graph, + ) diff --git a/src/writerai/types/__init__.py b/src/writerai/types/__init__.py index d04f7bf6..0e549136 100644 --- a/src/writerai/types/__init__.py +++ b/src/writerai/types/__init__.py @@ -3,9 +3,22 @@ from __future__ import annotations from .chat import Chat as Chat +from .file import File as File +from .graph import Graph as Graph from .completion import Completion as Completion from .streaming_data import StreamingData as StreamingData from .chat_chat_params import ChatChatParams as ChatChatParams +from .file_list_params import FileListParams as FileListParams +from .graph_list_params import GraphListParams as GraphListParams +from .file_upload_params import FileUploadParams as FileUploadParams from .chat_streaming_data import ChatStreamingData as ChatStreamingData +from .graph_create_params import GraphCreateParams as GraphCreateParams +from .graph_update_params import GraphUpdateParams as GraphUpdateParams from .model_list_response import ModelListResponse as ModelListResponse +from .file_delete_response import FileDeleteResponse as FileDeleteResponse +from .graph_create_response import GraphCreateResponse as GraphCreateResponse +from .graph_delete_response import GraphDeleteResponse as GraphDeleteResponse +from .graph_update_response import GraphUpdateResponse as GraphUpdateResponse from .completion_create_params import CompletionCreateParams as CompletionCreateParams +from .graph_add_file_to_graph_params import GraphAddFileToGraphParams as GraphAddFileToGraphParams +from .graph_remove_file_from_graph_response import GraphRemoveFileFromGraphResponse as GraphRemoveFileFromGraphResponse diff --git a/src/writerai/types/file.py b/src/writerai/types/file.py new file mode 100644 index 00000000..9448c403 --- /dev/null +++ b/src/writerai/types/file.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List +from datetime import datetime + +from .._models import BaseModel + +__all__ = ["File"] + + +class File(BaseModel): + id: str + + created_at: datetime + + graph_ids: List[str] + + name: str diff --git a/src/writerai/types/file_delete_response.py b/src/writerai/types/file_delete_response.py new file mode 100644 index 00000000..450d558c --- /dev/null +++ b/src/writerai/types/file_delete_response.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + + + +from .._models import BaseModel + +__all__ = ["FileDeleteResponse"] + + +class FileDeleteResponse(BaseModel): + id: str + + deleted: bool diff --git a/src/writerai/types/file_list_params.py b/src/writerai/types/file_list_params.py new file mode 100644 index 00000000..60866952 --- /dev/null +++ b/src/writerai/types/file_list_params.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, TypedDict + +__all__ = ["FileListParams"] + + +class FileListParams(TypedDict, total=False): + after: str + + before: str + + graph_id: str + + limit: int + + order: Literal["asc", "desc"] diff --git a/src/writerai/types/file_upload_params.py b/src/writerai/types/file_upload_params.py new file mode 100644 index 00000000..4a47b726 --- /dev/null +++ b/src/writerai/types/file_upload_params.py @@ -0,0 +1,20 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, Annotated, TypedDict + +from .._types import FileTypes +from .._utils import PropertyInfo + +__all__ = ["FileUploadParams"] + + +class FileUploadParams(TypedDict, total=False): + content: Required[FileTypes] + + content_disposition: Required[Annotated[str, PropertyInfo(alias="Content-Disposition")]] + + content_length: Required[Annotated[int, PropertyInfo(alias="Content-Length")]] + + content_type: Required[Annotated[str, PropertyInfo(alias="Content-Type")]] diff --git a/src/writerai/types/graph.py b/src/writerai/types/graph.py new file mode 100644 index 00000000..2045a5d0 --- /dev/null +++ b/src/writerai/types/graph.py @@ -0,0 +1,30 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime + +from .._models import BaseModel + +__all__ = ["Graph", "FileStatus"] + + +class FileStatus(BaseModel): + completed: int + + failed: int + + in_progress: int + + total: int + + +class Graph(BaseModel): + id: str + + created_at: datetime + + file_status: FileStatus + + name: str + + description: Optional[str] = None diff --git a/src/writerai/types/graph_add_file_to_graph_params.py b/src/writerai/types/graph_add_file_to_graph_params.py new file mode 100644 index 00000000..b1bab49a --- /dev/null +++ b/src/writerai/types/graph_add_file_to_graph_params.py @@ -0,0 +1,11 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["GraphAddFileToGraphParams"] + + +class GraphAddFileToGraphParams(TypedDict, total=False): + file_id: Required[str] diff --git a/src/writerai/types/graph_create_params.py b/src/writerai/types/graph_create_params.py new file mode 100644 index 00000000..24638cf3 --- /dev/null +++ b/src/writerai/types/graph_create_params.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["GraphCreateParams"] + + +class GraphCreateParams(TypedDict, total=False): + name: Required[str] + + description: str diff --git a/src/writerai/types/graph_create_response.py b/src/writerai/types/graph_create_response.py new file mode 100644 index 00000000..831d2982 --- /dev/null +++ b/src/writerai/types/graph_create_response.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime + +from .._models import BaseModel + +__all__ = ["GraphCreateResponse"] + + +class GraphCreateResponse(BaseModel): + id: str + + created_at: datetime + + name: str + + description: Optional[str] = None diff --git a/src/writerai/types/graph_delete_response.py b/src/writerai/types/graph_delete_response.py new file mode 100644 index 00000000..e689b600 --- /dev/null +++ b/src/writerai/types/graph_delete_response.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + + + +from .._models import BaseModel + +__all__ = ["GraphDeleteResponse"] + + +class GraphDeleteResponse(BaseModel): + id: str + + deleted: bool diff --git a/src/writerai/types/graph_list_params.py b/src/writerai/types/graph_list_params.py new file mode 100644 index 00000000..37668efc --- /dev/null +++ b/src/writerai/types/graph_list_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 Literal, TypedDict + +__all__ = ["GraphListParams"] + + +class GraphListParams(TypedDict, total=False): + after: str + + before: str + + limit: int + + order: Literal["asc", "desc"] diff --git a/src/writerai/types/graph_remove_file_from_graph_response.py b/src/writerai/types/graph_remove_file_from_graph_response.py new file mode 100644 index 00000000..bcf6d726 --- /dev/null +++ b/src/writerai/types/graph_remove_file_from_graph_response.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + + + +from .._models import BaseModel + +__all__ = ["GraphRemoveFileFromGraphResponse"] + + +class GraphRemoveFileFromGraphResponse(BaseModel): + id: str + + deleted: bool diff --git a/src/writerai/types/graph_update_params.py b/src/writerai/types/graph_update_params.py new file mode 100644 index 00000000..06ef5a98 --- /dev/null +++ b/src/writerai/types/graph_update_params.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["GraphUpdateParams"] + + +class GraphUpdateParams(TypedDict, total=False): + name: Required[str] + + description: str diff --git a/src/writerai/types/graph_update_response.py b/src/writerai/types/graph_update_response.py new file mode 100644 index 00000000..63a3f421 --- /dev/null +++ b/src/writerai/types/graph_update_response.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime + +from .._models import BaseModel + +__all__ = ["GraphUpdateResponse"] + + +class GraphUpdateResponse(BaseModel): + id: str + + created_at: datetime + + name: str + + description: Optional[str] = None diff --git a/src/writerai/types/model_list_response.py b/src/writerai/types/model_list_response.py index db27d548..cdec7b22 100644 --- a/src/writerai/types/model_list_response.py +++ b/src/writerai/types/model_list_response.py @@ -12,7 +12,7 @@ class Model(BaseModel): """The ID of the particular LLM that you want to use""" name: str - """The name of the particular LLM that you want to use""" + """The name of the particular LLM that you want to use.""" class ModelListResponse(BaseModel): diff --git a/tests/api_resources/test_files.py b/tests/api_resources/test_files.py new file mode 100644 index 00000000..7f19fff6 --- /dev/null +++ b/tests/api_resources/test_files.py @@ -0,0 +1,449 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import httpx +import pytest +from respx import MockRouter + +from writerai import Writer, AsyncWriter +from tests.utils import assert_matches_type +from writerai.types import File, FileDeleteResponse +from writerai._response import ( + BinaryAPIResponse, + AsyncBinaryAPIResponse, + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, +) +from writerai.pagination import SyncCursorPage, AsyncCursorPage + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestFiles: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_retrieve(self, client: Writer) -> None: + file = client.files.retrieve( + "string", + ) + assert_matches_type(File, file, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Writer) -> None: + response = client.files.with_raw_response.retrieve( + "string", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + file = response.parse() + assert_matches_type(File, file, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Writer) -> None: + with client.files.with_streaming_response.retrieve( + "string", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + file = response.parse() + assert_matches_type(File, file, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Writer) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): + client.files.with_raw_response.retrieve( + "", + ) + + @parametrize + def test_method_list(self, client: Writer) -> None: + file = client.files.list() + assert_matches_type(SyncCursorPage[File], file, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Writer) -> None: + file = client.files.list( + after="string", + before="string", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + limit=0, + order="asc", + ) + assert_matches_type(SyncCursorPage[File], file, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Writer) -> None: + response = client.files.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + file = response.parse() + assert_matches_type(SyncCursorPage[File], file, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Writer) -> None: + with client.files.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + file = response.parse() + assert_matches_type(SyncCursorPage[File], file, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_delete(self, client: Writer) -> None: + file = client.files.delete( + "string", + ) + assert_matches_type(FileDeleteResponse, file, path=["response"]) + + @parametrize + def test_raw_response_delete(self, client: Writer) -> None: + response = client.files.with_raw_response.delete( + "string", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + file = response.parse() + assert_matches_type(FileDeleteResponse, file, path=["response"]) + + @parametrize + def test_streaming_response_delete(self, client: Writer) -> None: + with client.files.with_streaming_response.delete( + "string", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + file = response.parse() + assert_matches_type(FileDeleteResponse, file, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_delete(self, client: Writer) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): + client.files.with_raw_response.delete( + "", + ) + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_method_download(self, client: Writer, respx_mock: MockRouter) -> None: + respx_mock.get("/v1/files/string/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + file = client.files.download( + "string", + ) + assert file.is_closed + assert file.json() == {"foo": "bar"} + assert cast(Any, file.is_closed) is True + assert isinstance(file, BinaryAPIResponse) + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_raw_response_download(self, client: Writer, respx_mock: MockRouter) -> None: + respx_mock.get("/v1/files/string/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + file = client.files.with_raw_response.download( + "string", + ) + + assert file.is_closed is True + assert file.http_request.headers.get("X-Stainless-Lang") == "python" + assert file.json() == {"foo": "bar"} + assert isinstance(file, BinaryAPIResponse) + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_streaming_response_download(self, client: Writer, respx_mock: MockRouter) -> None: + respx_mock.get("/v1/files/string/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + with client.files.with_streaming_response.download( + "string", + ) as file: + assert not file.is_closed + assert file.http_request.headers.get("X-Stainless-Lang") == "python" + + assert file.json() == {"foo": "bar"} + assert cast(Any, file.is_closed) is True + assert isinstance(file, StreamedBinaryAPIResponse) + + assert cast(Any, file.is_closed) is True + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_path_params_download(self, client: Writer) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): + client.files.with_raw_response.download( + "", + ) + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + def test_method_upload(self, client: Writer) -> None: + file = client.files.upload( + content=b"raw file contents", + content_disposition="string", + content_length=0, + content_type="string", + ) + assert_matches_type(File, file, path=["response"]) + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + def test_raw_response_upload(self, client: Writer) -> None: + response = client.files.with_raw_response.upload( + content=b"raw file contents", + content_disposition="string", + content_length=0, + content_type="string", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + file = response.parse() + assert_matches_type(File, file, path=["response"]) + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + def test_streaming_response_upload(self, client: Writer) -> None: + with client.files.with_streaming_response.upload( + content=b"raw file contents", + content_disposition="string", + content_length=0, + content_type="string", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + file = response.parse() + assert_matches_type(File, file, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncFiles: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_retrieve(self, async_client: AsyncWriter) -> None: + file = await async_client.files.retrieve( + "string", + ) + assert_matches_type(File, file, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncWriter) -> None: + response = await async_client.files.with_raw_response.retrieve( + "string", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + file = await response.parse() + assert_matches_type(File, file, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncWriter) -> None: + async with async_client.files.with_streaming_response.retrieve( + "string", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + file = await response.parse() + assert_matches_type(File, file, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncWriter) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): + await async_client.files.with_raw_response.retrieve( + "", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncWriter) -> None: + file = await async_client.files.list() + assert_matches_type(AsyncCursorPage[File], file, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncWriter) -> None: + file = await async_client.files.list( + after="string", + before="string", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + limit=0, + order="asc", + ) + assert_matches_type(AsyncCursorPage[File], file, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncWriter) -> None: + response = await async_client.files.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + file = await response.parse() + assert_matches_type(AsyncCursorPage[File], file, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncWriter) -> None: + async with async_client.files.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + file = await response.parse() + assert_matches_type(AsyncCursorPage[File], file, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_delete(self, async_client: AsyncWriter) -> None: + file = await async_client.files.delete( + "string", + ) + assert_matches_type(FileDeleteResponse, file, path=["response"]) + + @parametrize + async def test_raw_response_delete(self, async_client: AsyncWriter) -> None: + response = await async_client.files.with_raw_response.delete( + "string", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + file = await response.parse() + assert_matches_type(FileDeleteResponse, file, path=["response"]) + + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncWriter) -> None: + async with async_client.files.with_streaming_response.delete( + "string", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + file = await response.parse() + assert_matches_type(FileDeleteResponse, file, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_delete(self, async_client: AsyncWriter) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): + await async_client.files.with_raw_response.delete( + "", + ) + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_method_download(self, async_client: AsyncWriter, respx_mock: MockRouter) -> None: + respx_mock.get("/v1/files/string/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + file = await async_client.files.download( + "string", + ) + assert file.is_closed + assert await file.json() == {"foo": "bar"} + assert cast(Any, file.is_closed) is True + assert isinstance(file, AsyncBinaryAPIResponse) + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_raw_response_download(self, async_client: AsyncWriter, respx_mock: MockRouter) -> None: + respx_mock.get("/v1/files/string/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + file = await async_client.files.with_raw_response.download( + "string", + ) + + assert file.is_closed is True + assert file.http_request.headers.get("X-Stainless-Lang") == "python" + assert await file.json() == {"foo": "bar"} + assert isinstance(file, AsyncBinaryAPIResponse) + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_streaming_response_download(self, async_client: AsyncWriter, respx_mock: MockRouter) -> None: + respx_mock.get("/v1/files/string/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + async with async_client.files.with_streaming_response.download( + "string", + ) as file: + assert not file.is_closed + assert file.http_request.headers.get("X-Stainless-Lang") == "python" + + assert await file.json() == {"foo": "bar"} + assert cast(Any, file.is_closed) is True + assert isinstance(file, AsyncStreamedBinaryAPIResponse) + + assert cast(Any, file.is_closed) is True + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_path_params_download(self, async_client: AsyncWriter) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): + await async_client.files.with_raw_response.download( + "", + ) + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + async def test_method_upload(self, async_client: AsyncWriter) -> None: + file = await async_client.files.upload( + content=b"raw file contents", + content_disposition="string", + content_length=0, + content_type="string", + ) + assert_matches_type(File, file, path=["response"]) + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + async def test_raw_response_upload(self, async_client: AsyncWriter) -> None: + response = await async_client.files.with_raw_response.upload( + content=b"raw file contents", + content_disposition="string", + content_length=0, + content_type="string", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + file = await response.parse() + assert_matches_type(File, file, path=["response"]) + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + async def test_streaming_response_upload(self, async_client: AsyncWriter) -> None: + async with async_client.files.with_streaming_response.upload( + content=b"raw file contents", + content_disposition="string", + content_length=0, + content_type="string", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + file = await response.parse() + assert_matches_type(File, file, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_graphs.py b/tests/api_resources/test_graphs.py new file mode 100644 index 00000000..4c784dd7 --- /dev/null +++ b/tests/api_resources/test_graphs.py @@ -0,0 +1,612 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from writerai import Writer, AsyncWriter +from tests.utils import assert_matches_type +from writerai.types import ( + File, + Graph, + GraphCreateResponse, + GraphDeleteResponse, + GraphUpdateResponse, + GraphRemoveFileFromGraphResponse, +) +from writerai.pagination import SyncCursorPage, AsyncCursorPage + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestGraphs: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: Writer) -> None: + graph = client.graphs.create( + name="string", + ) + assert_matches_type(GraphCreateResponse, graph, path=["response"]) + + @parametrize + def test_method_create_with_all_params(self, client: Writer) -> None: + graph = client.graphs.create( + name="string", + description="string", + ) + assert_matches_type(GraphCreateResponse, graph, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Writer) -> None: + response = client.graphs.with_raw_response.create( + name="string", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + graph = response.parse() + assert_matches_type(GraphCreateResponse, graph, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Writer) -> None: + with client.graphs.with_streaming_response.create( + name="string", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + graph = response.parse() + assert_matches_type(GraphCreateResponse, graph, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_retrieve(self, client: Writer) -> None: + graph = client.graphs.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(Graph, graph, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Writer) -> None: + response = client.graphs.with_raw_response.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + graph = response.parse() + assert_matches_type(Graph, graph, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Writer) -> None: + with client.graphs.with_streaming_response.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + graph = response.parse() + assert_matches_type(Graph, graph, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Writer) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `graph_id` but received ''"): + client.graphs.with_raw_response.retrieve( + "", + ) + + @parametrize + def test_method_update(self, client: Writer) -> None: + graph = client.graphs.update( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="string", + ) + assert_matches_type(GraphUpdateResponse, graph, path=["response"]) + + @parametrize + def test_method_update_with_all_params(self, client: Writer) -> None: + graph = client.graphs.update( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="string", + description="string", + ) + assert_matches_type(GraphUpdateResponse, graph, path=["response"]) + + @parametrize + def test_raw_response_update(self, client: Writer) -> None: + response = client.graphs.with_raw_response.update( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="string", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + graph = response.parse() + assert_matches_type(GraphUpdateResponse, graph, path=["response"]) + + @parametrize + def test_streaming_response_update(self, client: Writer) -> None: + with client.graphs.with_streaming_response.update( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="string", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + graph = response.parse() + assert_matches_type(GraphUpdateResponse, graph, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_update(self, client: Writer) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `graph_id` but received ''"): + client.graphs.with_raw_response.update( + "", + name="string", + ) + + @parametrize + def test_method_list(self, client: Writer) -> None: + graph = client.graphs.list() + assert_matches_type(SyncCursorPage[Graph], graph, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Writer) -> None: + graph = client.graphs.list( + after="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + before="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + limit=0, + order="asc", + ) + assert_matches_type(SyncCursorPage[Graph], graph, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Writer) -> None: + response = client.graphs.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + graph = response.parse() + assert_matches_type(SyncCursorPage[Graph], graph, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Writer) -> None: + with client.graphs.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + graph = response.parse() + assert_matches_type(SyncCursorPage[Graph], graph, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_delete(self, client: Writer) -> None: + graph = client.graphs.delete( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(GraphDeleteResponse, graph, path=["response"]) + + @parametrize + def test_raw_response_delete(self, client: Writer) -> None: + response = client.graphs.with_raw_response.delete( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + graph = response.parse() + assert_matches_type(GraphDeleteResponse, graph, path=["response"]) + + @parametrize + def test_streaming_response_delete(self, client: Writer) -> None: + with client.graphs.with_streaming_response.delete( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + graph = response.parse() + assert_matches_type(GraphDeleteResponse, graph, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_delete(self, client: Writer) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `graph_id` but received ''"): + client.graphs.with_raw_response.delete( + "", + ) + + @parametrize + def test_method_add_file_to_graph(self, client: Writer) -> None: + graph = client.graphs.add_file_to_graph( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + file_id="string", + ) + assert_matches_type(File, graph, path=["response"]) + + @parametrize + def test_raw_response_add_file_to_graph(self, client: Writer) -> None: + response = client.graphs.with_raw_response.add_file_to_graph( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + file_id="string", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + graph = response.parse() + assert_matches_type(File, graph, path=["response"]) + + @parametrize + def test_streaming_response_add_file_to_graph(self, client: Writer) -> None: + with client.graphs.with_streaming_response.add_file_to_graph( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + file_id="string", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + graph = response.parse() + assert_matches_type(File, graph, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_add_file_to_graph(self, client: Writer) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `graph_id` but received ''"): + client.graphs.with_raw_response.add_file_to_graph( + "", + file_id="string", + ) + + @parametrize + def test_method_remove_file_from_graph(self, client: Writer) -> None: + graph = client.graphs.remove_file_from_graph( + "string", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(GraphRemoveFileFromGraphResponse, graph, path=["response"]) + + @parametrize + def test_raw_response_remove_file_from_graph(self, client: Writer) -> None: + response = client.graphs.with_raw_response.remove_file_from_graph( + "string", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + graph = response.parse() + assert_matches_type(GraphRemoveFileFromGraphResponse, graph, path=["response"]) + + @parametrize + def test_streaming_response_remove_file_from_graph(self, client: Writer) -> None: + with client.graphs.with_streaming_response.remove_file_from_graph( + "string", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + graph = response.parse() + assert_matches_type(GraphRemoveFileFromGraphResponse, graph, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_remove_file_from_graph(self, client: Writer) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `graph_id` but received ''"): + client.graphs.with_raw_response.remove_file_from_graph( + "string", + graph_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): + client.graphs.with_raw_response.remove_file_from_graph( + "", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + +class TestAsyncGraphs: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_create(self, async_client: AsyncWriter) -> None: + graph = await async_client.graphs.create( + name="string", + ) + assert_matches_type(GraphCreateResponse, graph, path=["response"]) + + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncWriter) -> None: + graph = await async_client.graphs.create( + name="string", + description="string", + ) + assert_matches_type(GraphCreateResponse, graph, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncWriter) -> None: + response = await async_client.graphs.with_raw_response.create( + name="string", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + graph = await response.parse() + assert_matches_type(GraphCreateResponse, graph, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncWriter) -> None: + async with async_client.graphs.with_streaming_response.create( + name="string", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + graph = await response.parse() + assert_matches_type(GraphCreateResponse, graph, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_retrieve(self, async_client: AsyncWriter) -> None: + graph = await async_client.graphs.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(Graph, graph, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncWriter) -> None: + response = await async_client.graphs.with_raw_response.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + graph = await response.parse() + assert_matches_type(Graph, graph, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncWriter) -> None: + async with async_client.graphs.with_streaming_response.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + graph = await response.parse() + assert_matches_type(Graph, graph, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncWriter) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `graph_id` but received ''"): + await async_client.graphs.with_raw_response.retrieve( + "", + ) + + @parametrize + async def test_method_update(self, async_client: AsyncWriter) -> None: + graph = await async_client.graphs.update( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="string", + ) + assert_matches_type(GraphUpdateResponse, graph, path=["response"]) + + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncWriter) -> None: + graph = await async_client.graphs.update( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="string", + description="string", + ) + assert_matches_type(GraphUpdateResponse, graph, path=["response"]) + + @parametrize + async def test_raw_response_update(self, async_client: AsyncWriter) -> None: + response = await async_client.graphs.with_raw_response.update( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="string", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + graph = await response.parse() + assert_matches_type(GraphUpdateResponse, graph, path=["response"]) + + @parametrize + async def test_streaming_response_update(self, async_client: AsyncWriter) -> None: + async with async_client.graphs.with_streaming_response.update( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="string", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + graph = await response.parse() + assert_matches_type(GraphUpdateResponse, graph, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_update(self, async_client: AsyncWriter) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `graph_id` but received ''"): + await async_client.graphs.with_raw_response.update( + "", + name="string", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncWriter) -> None: + graph = await async_client.graphs.list() + assert_matches_type(AsyncCursorPage[Graph], graph, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncWriter) -> None: + graph = await async_client.graphs.list( + after="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + before="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + limit=0, + order="asc", + ) + assert_matches_type(AsyncCursorPage[Graph], graph, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncWriter) -> None: + response = await async_client.graphs.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + graph = await response.parse() + assert_matches_type(AsyncCursorPage[Graph], graph, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncWriter) -> None: + async with async_client.graphs.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + graph = await response.parse() + assert_matches_type(AsyncCursorPage[Graph], graph, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_delete(self, async_client: AsyncWriter) -> None: + graph = await async_client.graphs.delete( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(GraphDeleteResponse, graph, path=["response"]) + + @parametrize + async def test_raw_response_delete(self, async_client: AsyncWriter) -> None: + response = await async_client.graphs.with_raw_response.delete( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + graph = await response.parse() + assert_matches_type(GraphDeleteResponse, graph, path=["response"]) + + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncWriter) -> None: + async with async_client.graphs.with_streaming_response.delete( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + graph = await response.parse() + assert_matches_type(GraphDeleteResponse, graph, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_delete(self, async_client: AsyncWriter) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `graph_id` but received ''"): + await async_client.graphs.with_raw_response.delete( + "", + ) + + @parametrize + async def test_method_add_file_to_graph(self, async_client: AsyncWriter) -> None: + graph = await async_client.graphs.add_file_to_graph( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + file_id="string", + ) + assert_matches_type(File, graph, path=["response"]) + + @parametrize + async def test_raw_response_add_file_to_graph(self, async_client: AsyncWriter) -> None: + response = await async_client.graphs.with_raw_response.add_file_to_graph( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + file_id="string", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + graph = await response.parse() + assert_matches_type(File, graph, path=["response"]) + + @parametrize + async def test_streaming_response_add_file_to_graph(self, async_client: AsyncWriter) -> None: + async with async_client.graphs.with_streaming_response.add_file_to_graph( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + file_id="string", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + graph = await response.parse() + assert_matches_type(File, graph, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_add_file_to_graph(self, async_client: AsyncWriter) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `graph_id` but received ''"): + await async_client.graphs.with_raw_response.add_file_to_graph( + "", + file_id="string", + ) + + @parametrize + async def test_method_remove_file_from_graph(self, async_client: AsyncWriter) -> None: + graph = await async_client.graphs.remove_file_from_graph( + "string", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(GraphRemoveFileFromGraphResponse, graph, path=["response"]) + + @parametrize + async def test_raw_response_remove_file_from_graph(self, async_client: AsyncWriter) -> None: + response = await async_client.graphs.with_raw_response.remove_file_from_graph( + "string", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + graph = await response.parse() + assert_matches_type(GraphRemoveFileFromGraphResponse, graph, path=["response"]) + + @parametrize + async def test_streaming_response_remove_file_from_graph(self, async_client: AsyncWriter) -> None: + async with async_client.graphs.with_streaming_response.remove_file_from_graph( + "string", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + graph = await response.parse() + assert_matches_type(GraphRemoveFileFromGraphResponse, graph, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_remove_file_from_graph(self, async_client: AsyncWriter) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `graph_id` but received ''"): + await async_client.graphs.with_raw_response.remove_file_from_graph( + "string", + graph_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): + await async_client.graphs.with_raw_response.remove_file_from_graph( + "", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) From a86d3d6c5a252f2dcc9d4da3338ddd0f91b5ea29 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 28 Jun 2024 17:58:28 +0000 Subject: [PATCH 030/399] feat(api): update via SDK Studio (#16) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 03ab73fe..2fce9461 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ It is generated with [Stainless](https://www.stainlessapi.com/). ## Documentation -The REST API documentation can be found [on dev.writer.com](https://dev.writer.com/api-guides/introduction). The full API of this library can be found in [api.md](api.md). +The REST API documentation can be found [on dev.writer.com](https://dev.writer.com/api-guides/introductio). The full API of this library can be found in [api.md](api.md). ## Installation From a50234d63ff8701c4d082dc5ea48ea62b746398d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 28 Jun 2024 17:58:49 +0000 Subject: [PATCH 031/399] feat(api): update via SDK Studio (#17) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2fce9461..03ab73fe 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ It is generated with [Stainless](https://www.stainlessapi.com/). ## Documentation -The REST API documentation can be found [on dev.writer.com](https://dev.writer.com/api-guides/introductio). The full API of this library can be found in [api.md](api.md). +The REST API documentation can be found [on dev.writer.com](https://dev.writer.com/api-guides/introduction). The full API of this library can be found in [api.md](api.md). ## Installation From dff5b5554e864073c28f0f6728cdfc51a54bb2a0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 28 Jun 2024 18:33:17 +0000 Subject: [PATCH 032/399] feat(api): update via SDK Studio (#18) --- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/release-doctor.yml | 2 +- CONTRIBUTING.md | 4 ++-- README.md | 6 +++--- pyproject.toml | 6 +++--- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index b850d240..e9c91ca5 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -1,6 +1,6 @@ # This workflow is triggered when a GitHub release is created. # It can also be run manually to re-publish to PyPI in case it failed for some reason. -# You can run this workflow by navigating to https://www.github.com/writerai/writer-python/actions/workflows/publish-pypi.yml +# You can run this workflow by navigating to https://www.github.com/writer/writer-python/actions/workflows/publish-pypi.yml name: Publish PyPI on: workflow_dispatch: diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index 33a5d613..93c5ed1a 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -7,7 +7,7 @@ jobs: release_doctor: name: release doctor runs-on: ubuntu-latest - if: github.repository == 'writerai/writer-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') + if: github.repository == 'writer/writer-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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d41926b0..ac2a7dfa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,7 +59,7 @@ If you’d like to use the repository from source, you can either install from g To install via git: ```bash -pip install git+ssh://git@github.com/writerai/writer-python.git +pip install git+ssh://git@github.com/writer/writer-python.git ``` Alternatively, you can build from source and install the wheel file: @@ -117,7 +117,7 @@ the changes aren't made through the automated pipeline, you may want to make rel ### Publish with a GitHub workflow -You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/writerai/writer-python/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. +You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/writer/writer-python/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. ### Publish manually diff --git a/README.md b/README.md index 03ab73fe..331c8783 100644 --- a/README.md +++ b/README.md @@ -283,9 +283,9 @@ chat = response.parse() # get the object that `chat.chat()` would have returned print(chat.id) ``` -These methods return an [`APIResponse`](https://github.com/writerai/writer-python/tree/main/src/writerai/_response.py) object. +These methods return an [`APIResponse`](https://github.com/writer/writer-python/tree/main/src/writerai/_response.py) object. -The async client returns an [`AsyncAPIResponse`](https://github.com/writerai/writer-python/tree/main/src/writerai/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. +The async client returns an [`AsyncAPIResponse`](https://github.com/writer/writer-python/tree/main/src/writerai/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. #### `.with_streaming_response` @@ -381,7 +381,7 @@ This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) con We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. -We are keen for your feedback; please open an [issue](https://www.github.com/writerai/writer-python/issues) with questions, bugs, or suggestions. +We are keen for your feedback; please open an [issue](https://www.github.com/writer/writer-python/issues) with questions, bugs, or suggestions. ## Requirements diff --git a/pyproject.toml b/pyproject.toml index 6d2d20a6..3ee97cdc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,8 +39,8 @@ classifiers = [ [project.urls] -Homepage = "https://github.com/writerai/writer-python" -Repository = "https://github.com/writerai/writer-python" +Homepage = "https://github.com/writer/writer-python" +Repository = "https://github.com/writer/writer-python" @@ -123,7 +123,7 @@ path = "README.md" [[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]] # replace relative links with absolute links pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)' -replacement = '[\1](https://github.com/writerai/writer-python/tree/main/\g<2>)' +replacement = '[\1](https://github.com/writer/writer-python/tree/main/\g<2>)' [tool.black] line-length = 120 From 3708a81798e06f8f80dcf92a1d9f7528971da871 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 10 Jul 2024 07:41:50 +0000 Subject: [PATCH 033/399] chore(internal): version bump (#19) --- .devcontainer/Dockerfile | 2 +- .github/workflows/ci.yml | 4 +-- .github/workflows/publish-pypi.yml | 4 +-- .release-please-manifest.json | 2 +- pyproject.toml | 3 ++- requirements-dev.lock | 8 ++++++ requirements.lock | 1 + src/writerai/_base_client.py | 42 ++++++++++++++++++++++++------ src/writerai/_models.py | 27 +++++++++++++++++++ src/writerai/_version.py | 2 +- 10 files changed, 79 insertions(+), 16 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 83bca8f7..ac9a2e75 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -3,7 +3,7 @@ FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} USER vscode -RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.24.0" RYE_INSTALL_OPTION="--yes" bash +RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.35.0" RYE_INSTALL_OPTION="--yes" bash ENV PATH=/home/vscode/.rye/shims:$PATH RUN echo "[[ -d .venv ]] && source .venv/bin/activate" >> /home/vscode/.bashrc diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c339440..257f0561 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: curl -sSf https://rye.astral.sh/get | bash echo "$HOME/.rye/shims" >> $GITHUB_PATH env: - RYE_VERSION: 0.24.0 + RYE_VERSION: '0.35.0' RYE_INSTALL_OPTION: '--yes' - name: Install dependencies @@ -41,7 +41,7 @@ jobs: curl -sSf https://rye.astral.sh/get | bash echo "$HOME/.rye/shims" >> $GITHUB_PATH env: - RYE_VERSION: 0.24.0 + RYE_VERSION: '0.35.0' RYE_INSTALL_OPTION: '--yes' - name: Bootstrap diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index e9c91ca5..e206653d 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -21,8 +21,8 @@ jobs: curl -sSf https://rye.astral.sh/get | bash echo "$HOME/.rye/shims" >> $GITHUB_PATH env: - RYE_VERSION: 0.24.0 - RYE_INSTALL_OPTION: "--yes" + RYE_VERSION: '0.35.0' + RYE_INSTALL_OPTION: '--yes' - name: Publish to PyPI run: | diff --git a/.release-please-manifest.json b/.release-please-manifest.json index cda9cbdf..10f30916 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.2" + ".": "0.2.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 3ee97cdc..43a9ace2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "0.1.2" +version = "0.2.0" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" @@ -58,6 +58,7 @@ dev-dependencies = [ "nox", "dirty-equals>=0.6.0", "importlib-metadata>=6.7.0", + "rich>=13.7.1", ] diff --git a/requirements-dev.lock b/requirements-dev.lock index 70a53ed3..71f90a8b 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -6,6 +6,7 @@ # features: [] # all-features: true # with-sources: false +# generate-hashes: false -e file:. annotated-types==0.6.0 @@ -44,6 +45,10 @@ idna==3.4 importlib-metadata==7.0.0 iniconfig==2.0.0 # via pytest +markdown-it-py==3.0.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py mypy==1.7.1 mypy-extensions==1.0.0 # via mypy @@ -63,6 +68,8 @@ pydantic==2.7.1 # via writer-sdk pydantic-core==2.18.2 # via pydantic +pygments==2.18.0 + # via rich pyright==1.1.364 pytest==7.1.1 # via pytest-asyncio @@ -72,6 +79,7 @@ python-dateutil==2.8.2 pytz==2023.3.post1 # via dirty-equals respx==0.20.2 +rich==13.7.1 ruff==0.1.9 setuptools==68.2.2 # via nodeenv diff --git a/requirements.lock b/requirements.lock index bd53c624..b2984eab 100644 --- a/requirements.lock +++ b/requirements.lock @@ -6,6 +6,7 @@ # features: [] # all-features: true # with-sources: false +# generate-hashes: false -e file:. annotated-types==0.6.0 diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index 5665fda9..920075c8 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -58,6 +58,7 @@ HttpxSendArgs, AsyncTransport, RequestOptions, + HttpxRequestFiles, ModelBuilderProtocol, ) from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping @@ -459,6 +460,7 @@ def _build_request( headers = self._build_headers(options) params = _merge_mappings(self.default_query, options.params) content_type = headers.get("Content-Type") + files = options.files # If the given Content-Type header is multipart/form-data then it # has to be removed so that httpx can generate the header with @@ -472,7 +474,7 @@ def _build_request( headers.pop("Content-Type") # As we are now sending multipart/form-data instead of application/json - # we need to tell httpx to use it, https://www.python-httpx.org/advanced/#multipart-file-encoding + # we need to tell httpx to use it, https://www.python-httpx.org/advanced/clients/#multipart-file-encoding if json_data: if not is_dict(json_data): raise TypeError( @@ -480,6 +482,15 @@ def _build_request( ) kwargs["data"] = self._serialize_multipartform(json_data) + # httpx determines whether or not to send a "multipart/form-data" + # request based on the truthiness of the "files" argument. + # This gets around that issue by generating a dict value that + # evaluates to true. + # + # https://github.com/encode/httpx/discussions/2399#discussioncomment-3814186 + if not files: + files = cast(HttpxRequestFiles, ForceMultipartDict()) + # TODO: report this error to httpx return self._client.build_request( # pyright: ignore[reportUnknownMemberType] headers=headers, @@ -492,7 +503,7 @@ def _build_request( # https://github.com/microsoft/pyright/issues/3526#event-6715453066 params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None, json=json_data, - files=options.files, + files=files, **kwargs, ) @@ -944,6 +955,11 @@ def _request( stream: bool, stream_cls: type[_StreamT] | None, ) -> ResponseT | _StreamT: + # create a copy of the options we were given so that if the + # options are mutated later & we then retry, the retries are + # given the original options + input_options = model_copy(options) + cast_to = self._maybe_override_cast_to(cast_to, options) self._prepare_options(options) @@ -968,7 +984,7 @@ def _request( if retries > 0: return self._retry_request( - options, + input_options, cast_to, retries, stream=stream, @@ -983,7 +999,7 @@ def _request( if retries > 0: return self._retry_request( - options, + input_options, cast_to, retries, stream=stream, @@ -1011,7 +1027,7 @@ def _request( if retries > 0 and self._should_retry(err.response): err.response.close() return self._retry_request( - options, + input_options, cast_to, retries, err.response.headers, @@ -1507,6 +1523,11 @@ async def _request( # execute it earlier while we are in an async context self._platform = await asyncify(get_platform)() + # create a copy of the options we were given so that if the + # options are mutated later & we then retry, the retries are + # given the original options + input_options = model_copy(options) + cast_to = self._maybe_override_cast_to(cast_to, options) await self._prepare_options(options) @@ -1529,7 +1550,7 @@ async def _request( if retries > 0: return await self._retry_request( - options, + input_options, cast_to, retries, stream=stream, @@ -1544,7 +1565,7 @@ async def _request( if retries > 0: return await self._retry_request( - options, + input_options, cast_to, retries, stream=stream, @@ -1567,7 +1588,7 @@ async def _request( if retries > 0 and self._should_retry(err.response): await err.response.aclose() return await self._retry_request( - options, + input_options, cast_to, retries, err.response.headers, @@ -1863,6 +1884,11 @@ def make_request_options( return options +class ForceMultipartDict(Dict[str, None]): + def __bool__(self) -> bool: + return True + + class OtherPlatform: def __init__(self, name: str) -> None: self.name = name diff --git a/src/writerai/_models.py b/src/writerai/_models.py index 75c68cc7..5d95bb4b 100644 --- a/src/writerai/_models.py +++ b/src/writerai/_models.py @@ -10,6 +10,7 @@ ClassVar, Protocol, Required, + ParamSpec, TypedDict, TypeGuard, final, @@ -67,6 +68,9 @@ __all__ = ["BaseModel", "GenericModel"] _T = TypeVar("_T") +_BaseModelT = TypeVar("_BaseModelT", bound="BaseModel") + +P = ParamSpec("P") @runtime_checkable @@ -379,6 +383,29 @@ def is_basemodel_type(type_: type) -> TypeGuard[type[BaseModel] | type[GenericMo return issubclass(origin, BaseModel) or issubclass(origin, GenericModel) +def build( + base_model_cls: Callable[P, _BaseModelT], + *args: P.args, + **kwargs: P.kwargs, +) -> _BaseModelT: + """Construct a BaseModel class without validation. + + This is useful for cases where you need to instantiate a `BaseModel` + from an API response as this provides type-safe params which isn't supported + by helpers like `construct_type()`. + + ```py + build(MyModel, my_field_a="foo", my_field_b=123) + ``` + """ + if args: + raise TypeError( + "Received positional arguments which are not supported; Keyword arguments must be used instead", + ) + + return cast(_BaseModelT, construct_type(type_=base_model_cls, value=kwargs)) + + def construct_type(*, value: object, type_: object) -> object: """Loose coercion to the expected type with construction of nested values. diff --git a/src/writerai/_version.py b/src/writerai/_version.py index 1d37fed3..11c5aafa 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "0.1.2" # x-release-please-version +__version__ = "0.2.0" # x-release-please-version From d68c05c3803d357367aec93bff5d3d6e13d3f626 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 12 Jul 2024 08:06:43 +0000 Subject: [PATCH 034/399] feat(api): update via SDK Studio (#20) --- .github/workflows/ci.yml | 1 + README.md | 14 ++-- requirements-dev.lock | 2 +- src/writerai/_models.py | 8 ++ src/writerai/resources/chat.py | 4 +- src/writerai/resources/completions.py | 4 +- src/writerai/resources/files.py | 5 +- src/writerai/resources/graphs.py | 5 +- src/writerai/resources/models.py | 4 +- tests/api_resources/test_chat.py | 40 ++++----- tests/api_resources/test_files.py | 80 +++++++++--------- tests/api_resources/test_graphs.py | 116 +++++++++++++------------- tests/test_client.py | 8 +- 13 files changed, 144 insertions(+), 147 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 257f0561..40293964 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,7 @@ on: pull_request: branches: - main + - next jobs: lint: diff --git a/README.md b/README.md index 331c8783..2d543d65 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ client = Writer( chat = client.chat.chat( messages=[ { - "content": "string", + "content": "content", "role": "user", } ], @@ -68,7 +68,7 @@ async def main() -> None: chat = await client.chat.chat( messages=[ { - "content": "string", + "content": "content", "role": "user", } ], @@ -144,7 +144,7 @@ try: client.chat.chat( messages=[ { - "content": "string", + "content": "content", "role": "user", } ], @@ -195,7 +195,7 @@ client = Writer( client.with_options(max_retries=5).chat.chat( messages=[ { - "content": "string", + "content": "content", "role": "user", } ], @@ -226,7 +226,7 @@ client = Writer( client.with_options(timeout=5.0).chat.chat( messages=[ { - "content": "string", + "content": "content", "role": "user", } ], @@ -272,7 +272,7 @@ from writerai import Writer client = Writer() response = client.chat.with_raw_response.chat( messages=[{ - "content": "string", + "content": "content", "role": "user", }], model="palmyra-x-32k", @@ -297,7 +297,7 @@ To stream the response body, use `.with_streaming_response` instead, which requi with client.chat.with_streaming_response.chat( messages=[ { - "content": "string", + "content": "content", "role": "user", } ], diff --git a/requirements-dev.lock b/requirements-dev.lock index 71f90a8b..d9332cf2 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -49,7 +49,7 @@ markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -mypy==1.7.1 +mypy==1.10.1 mypy-extensions==1.0.0 # via mypy nodeenv==1.8.0 diff --git a/src/writerai/_models.py b/src/writerai/_models.py index 5d95bb4b..eb7ce3bd 100644 --- a/src/writerai/_models.py +++ b/src/writerai/_models.py @@ -643,6 +643,14 @@ def validate_type(*, type_: type[_T], value: object) -> _T: return cast(_T, _validate_non_model_type(type_=type_, value=value)) +def set_pydantic_config(typ: Any, config: pydantic.ConfigDict) -> None: + """Add a pydantic config for the given type. + + Note: this is a no-op on Pydantic v1. + """ + setattr(typ, "__pydantic_config__", config) # noqa: B010 + + # our use of subclasssing here causes weirdness for type checkers, # so we just pretend that we don't subclass if TYPE_CHECKING: diff --git a/src/writerai/resources/chat.py b/src/writerai/resources/chat.py index c2f21f1d..702651e0 100644 --- a/src/writerai/resources/chat.py +++ b/src/writerai/resources/chat.py @@ -24,9 +24,7 @@ ) from .._streaming import Stream, AsyncStream from ..types.chat import Chat -from .._base_client import ( - make_request_options, -) +from .._base_client import make_request_options from ..types.chat_streaming_data import ChatStreamingData __all__ = ["ChatResource", "AsyncChatResource"] diff --git a/src/writerai/resources/completions.py b/src/writerai/resources/completions.py index 9e773db3..6962f7b1 100644 --- a/src/writerai/resources/completions.py +++ b/src/writerai/resources/completions.py @@ -23,9 +23,7 @@ async_to_streamed_response_wrapper, ) from .._streaming import Stream, AsyncStream -from .._base_client import ( - make_request_options, -) +from .._base_client import make_request_options from ..types.completion import Completion from ..types.streaming_data import StreamingData diff --git a/src/writerai/resources/files.py b/src/writerai/resources/files.py index 64fc8428..3ecec443 100644 --- a/src/writerai/resources/files.py +++ b/src/writerai/resources/files.py @@ -30,10 +30,7 @@ ) from ..pagination import SyncCursorPage, AsyncCursorPage from ..types.file import File -from .._base_client import ( - AsyncPaginator, - make_request_options, -) +from .._base_client import AsyncPaginator, make_request_options from ..types.file_delete_response import FileDeleteResponse __all__ = ["FilesResource", "AsyncFilesResource"] diff --git a/src/writerai/resources/graphs.py b/src/writerai/resources/graphs.py index 73c7add7..f3f087cf 100644 --- a/src/writerai/resources/graphs.py +++ b/src/writerai/resources/graphs.py @@ -28,10 +28,7 @@ from ..pagination import SyncCursorPage, AsyncCursorPage from ..types.file import File from ..types.graph import Graph -from .._base_client import ( - AsyncPaginator, - make_request_options, -) +from .._base_client import AsyncPaginator, make_request_options from ..types.graph_create_response import GraphCreateResponse from ..types.graph_delete_response import GraphDeleteResponse from ..types.graph_update_response import GraphUpdateResponse diff --git a/src/writerai/resources/models.py b/src/writerai/resources/models.py index bcf0d359..14028e91 100644 --- a/src/writerai/resources/models.py +++ b/src/writerai/resources/models.py @@ -13,9 +13,7 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from .._base_client import ( - make_request_options, -) +from .._base_client import make_request_options from ..types.model_list_response import ModelListResponse __all__ = ["ModelsResource", "AsyncModelsResource"] diff --git a/tests/api_resources/test_chat.py b/tests/api_resources/test_chat.py index deaa8fc4..5789a046 100644 --- a/tests/api_resources/test_chat.py +++ b/tests/api_resources/test_chat.py @@ -22,7 +22,7 @@ def test_method_chat_overload_1(self, client: Writer) -> None: chat = client.chat.chat( messages=[ { - "content": "string", + "content": "content", "role": "user", } ], @@ -35,9 +35,9 @@ def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: chat = client.chat.chat( messages=[ { - "content": "string", + "content": "content", "role": "user", - "name": "string", + "name": "name", } ], model="palmyra-x-002-32k", @@ -55,7 +55,7 @@ def test_raw_response_chat_overload_1(self, client: Writer) -> None: response = client.chat.with_raw_response.chat( messages=[ { - "content": "string", + "content": "content", "role": "user", } ], @@ -72,7 +72,7 @@ def test_streaming_response_chat_overload_1(self, client: Writer) -> None: with client.chat.with_streaming_response.chat( messages=[ { - "content": "string", + "content": "content", "role": "user", } ], @@ -91,7 +91,7 @@ def test_method_chat_overload_2(self, client: Writer) -> None: chat_stream = client.chat.chat( messages=[ { - "content": "string", + "content": "content", "role": "user", } ], @@ -105,9 +105,9 @@ def test_method_chat_with_all_params_overload_2(self, client: Writer) -> None: chat_stream = client.chat.chat( messages=[ { - "content": "string", + "content": "content", "role": "user", - "name": "string", + "name": "name", } ], model="palmyra-x-002-32k", @@ -125,7 +125,7 @@ def test_raw_response_chat_overload_2(self, client: Writer) -> None: response = client.chat.with_raw_response.chat( messages=[ { - "content": "string", + "content": "content", "role": "user", } ], @@ -142,7 +142,7 @@ def test_streaming_response_chat_overload_2(self, client: Writer) -> None: with client.chat.with_streaming_response.chat( messages=[ { - "content": "string", + "content": "content", "role": "user", } ], @@ -166,7 +166,7 @@ async def test_method_chat_overload_1(self, async_client: AsyncWriter) -> None: chat = await async_client.chat.chat( messages=[ { - "content": "string", + "content": "content", "role": "user", } ], @@ -179,9 +179,9 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW chat = await async_client.chat.chat( messages=[ { - "content": "string", + "content": "content", "role": "user", - "name": "string", + "name": "name", } ], model="palmyra-x-002-32k", @@ -199,7 +199,7 @@ async def test_raw_response_chat_overload_1(self, async_client: AsyncWriter) -> response = await async_client.chat.with_raw_response.chat( messages=[ { - "content": "string", + "content": "content", "role": "user", } ], @@ -216,7 +216,7 @@ async def test_streaming_response_chat_overload_1(self, async_client: AsyncWrite async with async_client.chat.with_streaming_response.chat( messages=[ { - "content": "string", + "content": "content", "role": "user", } ], @@ -235,7 +235,7 @@ async def test_method_chat_overload_2(self, async_client: AsyncWriter) -> None: chat_stream = await async_client.chat.chat( messages=[ { - "content": "string", + "content": "content", "role": "user", } ], @@ -249,9 +249,9 @@ async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncW chat_stream = await async_client.chat.chat( messages=[ { - "content": "string", + "content": "content", "role": "user", - "name": "string", + "name": "name", } ], model="palmyra-x-002-32k", @@ -269,7 +269,7 @@ async def test_raw_response_chat_overload_2(self, async_client: AsyncWriter) -> response = await async_client.chat.with_raw_response.chat( messages=[ { - "content": "string", + "content": "content", "role": "user", } ], @@ -286,7 +286,7 @@ async def test_streaming_response_chat_overload_2(self, async_client: AsyncWrite async with async_client.chat.with_streaming_response.chat( messages=[ { - "content": "string", + "content": "content", "role": "user", } ], diff --git a/tests/api_resources/test_files.py b/tests/api_resources/test_files.py index 7f19fff6..aaa93d8d 100644 --- a/tests/api_resources/test_files.py +++ b/tests/api_resources/test_files.py @@ -29,14 +29,14 @@ class TestFiles: @parametrize def test_method_retrieve(self, client: Writer) -> None: file = client.files.retrieve( - "string", + "fileId", ) assert_matches_type(File, file, path=["response"]) @parametrize def test_raw_response_retrieve(self, client: Writer) -> None: response = client.files.with_raw_response.retrieve( - "string", + "fileId", ) assert response.is_closed is True @@ -47,7 +47,7 @@ def test_raw_response_retrieve(self, client: Writer) -> None: @parametrize def test_streaming_response_retrieve(self, client: Writer) -> None: with client.files.with_streaming_response.retrieve( - "string", + "fileId", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -72,8 +72,8 @@ def test_method_list(self, client: Writer) -> None: @parametrize def test_method_list_with_all_params(self, client: Writer) -> None: file = client.files.list( - after="string", - before="string", + after="after", + before="before", graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", limit=0, order="asc", @@ -103,14 +103,14 @@ def test_streaming_response_list(self, client: Writer) -> None: @parametrize def test_method_delete(self, client: Writer) -> None: file = client.files.delete( - "string", + "fileId", ) assert_matches_type(FileDeleteResponse, file, path=["response"]) @parametrize def test_raw_response_delete(self, client: Writer) -> None: response = client.files.with_raw_response.delete( - "string", + "fileId", ) assert response.is_closed is True @@ -121,7 +121,7 @@ def test_raw_response_delete(self, client: Writer) -> None: @parametrize def test_streaming_response_delete(self, client: Writer) -> None: with client.files.with_streaming_response.delete( - "string", + "fileId", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -142,9 +142,9 @@ def test_path_params_delete(self, client: Writer) -> None: @parametrize @pytest.mark.respx(base_url=base_url) def test_method_download(self, client: Writer, respx_mock: MockRouter) -> None: - respx_mock.get("/v1/files/string/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + respx_mock.get("/v1/files/fileId/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) file = client.files.download( - "string", + "fileId", ) assert file.is_closed assert file.json() == {"foo": "bar"} @@ -155,10 +155,10 @@ def test_method_download(self, client: Writer, respx_mock: MockRouter) -> None: @parametrize @pytest.mark.respx(base_url=base_url) def test_raw_response_download(self, client: Writer, respx_mock: MockRouter) -> None: - respx_mock.get("/v1/files/string/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + respx_mock.get("/v1/files/fileId/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) file = client.files.with_raw_response.download( - "string", + "fileId", ) assert file.is_closed is True @@ -170,9 +170,9 @@ def test_raw_response_download(self, client: Writer, respx_mock: MockRouter) -> @parametrize @pytest.mark.respx(base_url=base_url) def test_streaming_response_download(self, client: Writer, respx_mock: MockRouter) -> None: - respx_mock.get("/v1/files/string/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + respx_mock.get("/v1/files/fileId/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) with client.files.with_streaming_response.download( - "string", + "fileId", ) as file: assert not file.is_closed assert file.http_request.headers.get("X-Stainless-Lang") == "python" @@ -197,9 +197,9 @@ def test_path_params_download(self, client: Writer) -> None: def test_method_upload(self, client: Writer) -> None: file = client.files.upload( content=b"raw file contents", - content_disposition="string", + content_disposition="Content-Disposition", content_length=0, - content_type="string", + content_type="Content-Type", ) assert_matches_type(File, file, path=["response"]) @@ -208,9 +208,9 @@ def test_method_upload(self, client: Writer) -> None: def test_raw_response_upload(self, client: Writer) -> None: response = client.files.with_raw_response.upload( content=b"raw file contents", - content_disposition="string", + content_disposition="Content-Disposition", content_length=0, - content_type="string", + content_type="Content-Type", ) assert response.is_closed is True @@ -223,9 +223,9 @@ def test_raw_response_upload(self, client: Writer) -> None: def test_streaming_response_upload(self, client: Writer) -> None: with client.files.with_streaming_response.upload( content=b"raw file contents", - content_disposition="string", + content_disposition="Content-Disposition", content_length=0, - content_type="string", + content_type="Content-Type", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -242,14 +242,14 @@ class TestAsyncFiles: @parametrize async def test_method_retrieve(self, async_client: AsyncWriter) -> None: file = await async_client.files.retrieve( - "string", + "fileId", ) assert_matches_type(File, file, path=["response"]) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncWriter) -> None: response = await async_client.files.with_raw_response.retrieve( - "string", + "fileId", ) assert response.is_closed is True @@ -260,7 +260,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncWriter) -> None: @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncWriter) -> None: async with async_client.files.with_streaming_response.retrieve( - "string", + "fileId", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -285,8 +285,8 @@ async def test_method_list(self, async_client: AsyncWriter) -> None: @parametrize async def test_method_list_with_all_params(self, async_client: AsyncWriter) -> None: file = await async_client.files.list( - after="string", - before="string", + after="after", + before="before", graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", limit=0, order="asc", @@ -316,14 +316,14 @@ async def test_streaming_response_list(self, async_client: AsyncWriter) -> None: @parametrize async def test_method_delete(self, async_client: AsyncWriter) -> None: file = await async_client.files.delete( - "string", + "fileId", ) assert_matches_type(FileDeleteResponse, file, path=["response"]) @parametrize async def test_raw_response_delete(self, async_client: AsyncWriter) -> None: response = await async_client.files.with_raw_response.delete( - "string", + "fileId", ) assert response.is_closed is True @@ -334,7 +334,7 @@ async def test_raw_response_delete(self, async_client: AsyncWriter) -> None: @parametrize async def test_streaming_response_delete(self, async_client: AsyncWriter) -> None: async with async_client.files.with_streaming_response.delete( - "string", + "fileId", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -355,9 +355,9 @@ async def test_path_params_delete(self, async_client: AsyncWriter) -> None: @parametrize @pytest.mark.respx(base_url=base_url) async def test_method_download(self, async_client: AsyncWriter, respx_mock: MockRouter) -> None: - respx_mock.get("/v1/files/string/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + respx_mock.get("/v1/files/fileId/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) file = await async_client.files.download( - "string", + "fileId", ) assert file.is_closed assert await file.json() == {"foo": "bar"} @@ -368,10 +368,10 @@ async def test_method_download(self, async_client: AsyncWriter, respx_mock: Mock @parametrize @pytest.mark.respx(base_url=base_url) async def test_raw_response_download(self, async_client: AsyncWriter, respx_mock: MockRouter) -> None: - respx_mock.get("/v1/files/string/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + respx_mock.get("/v1/files/fileId/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) file = await async_client.files.with_raw_response.download( - "string", + "fileId", ) assert file.is_closed is True @@ -383,9 +383,9 @@ async def test_raw_response_download(self, async_client: AsyncWriter, respx_mock @parametrize @pytest.mark.respx(base_url=base_url) async def test_streaming_response_download(self, async_client: AsyncWriter, respx_mock: MockRouter) -> None: - respx_mock.get("/v1/files/string/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + respx_mock.get("/v1/files/fileId/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) async with async_client.files.with_streaming_response.download( - "string", + "fileId", ) as file: assert not file.is_closed assert file.http_request.headers.get("X-Stainless-Lang") == "python" @@ -410,9 +410,9 @@ async def test_path_params_download(self, async_client: AsyncWriter) -> None: async def test_method_upload(self, async_client: AsyncWriter) -> None: file = await async_client.files.upload( content=b"raw file contents", - content_disposition="string", + content_disposition="Content-Disposition", content_length=0, - content_type="string", + content_type="Content-Type", ) assert_matches_type(File, file, path=["response"]) @@ -421,9 +421,9 @@ async def test_method_upload(self, async_client: AsyncWriter) -> None: async def test_raw_response_upload(self, async_client: AsyncWriter) -> None: response = await async_client.files.with_raw_response.upload( content=b"raw file contents", - content_disposition="string", + content_disposition="Content-Disposition", content_length=0, - content_type="string", + content_type="Content-Type", ) assert response.is_closed is True @@ -436,9 +436,9 @@ async def test_raw_response_upload(self, async_client: AsyncWriter) -> None: async def test_streaming_response_upload(self, async_client: AsyncWriter) -> None: async with async_client.files.with_streaming_response.upload( content=b"raw file contents", - content_disposition="string", + content_disposition="Content-Disposition", content_length=0, - content_type="string", + content_type="Content-Type", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" diff --git a/tests/api_resources/test_graphs.py b/tests/api_resources/test_graphs.py index 4c784dd7..a25da3a4 100644 --- a/tests/api_resources/test_graphs.py +++ b/tests/api_resources/test_graphs.py @@ -28,22 +28,22 @@ class TestGraphs: @parametrize def test_method_create(self, client: Writer) -> None: graph = client.graphs.create( - name="string", + name="name", ) assert_matches_type(GraphCreateResponse, graph, path=["response"]) @parametrize def test_method_create_with_all_params(self, client: Writer) -> None: graph = client.graphs.create( - name="string", - description="string", + name="name", + description="description", ) assert_matches_type(GraphCreateResponse, graph, path=["response"]) @parametrize def test_raw_response_create(self, client: Writer) -> None: response = client.graphs.with_raw_response.create( - name="string", + name="name", ) assert response.is_closed is True @@ -54,7 +54,7 @@ def test_raw_response_create(self, client: Writer) -> None: @parametrize def test_streaming_response_create(self, client: Writer) -> None: with client.graphs.with_streaming_response.create( - name="string", + name="name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -105,25 +105,25 @@ def test_path_params_retrieve(self, client: Writer) -> None: @parametrize def test_method_update(self, client: Writer) -> None: graph = client.graphs.update( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - name="string", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", ) assert_matches_type(GraphUpdateResponse, graph, path=["response"]) @parametrize def test_method_update_with_all_params(self, client: Writer) -> None: graph = client.graphs.update( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - name="string", - description="string", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + description="description", ) assert_matches_type(GraphUpdateResponse, graph, path=["response"]) @parametrize def test_raw_response_update(self, client: Writer) -> None: response = client.graphs.with_raw_response.update( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - name="string", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", ) assert response.is_closed is True @@ -134,8 +134,8 @@ def test_raw_response_update(self, client: Writer) -> None: @parametrize def test_streaming_response_update(self, client: Writer) -> None: with client.graphs.with_streaming_response.update( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - name="string", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -149,8 +149,8 @@ def test_streaming_response_update(self, client: Writer) -> None: def test_path_params_update(self, client: Writer) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `graph_id` but received ''"): client.graphs.with_raw_response.update( - "", - name="string", + graph_id="", + name="name", ) @parametrize @@ -229,16 +229,16 @@ def test_path_params_delete(self, client: Writer) -> None: @parametrize def test_method_add_file_to_graph(self, client: Writer) -> None: graph = client.graphs.add_file_to_graph( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - file_id="string", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + file_id="file_id", ) assert_matches_type(File, graph, path=["response"]) @parametrize def test_raw_response_add_file_to_graph(self, client: Writer) -> None: response = client.graphs.with_raw_response.add_file_to_graph( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - file_id="string", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + file_id="file_id", ) assert response.is_closed is True @@ -249,8 +249,8 @@ def test_raw_response_add_file_to_graph(self, client: Writer) -> None: @parametrize def test_streaming_response_add_file_to_graph(self, client: Writer) -> None: with client.graphs.with_streaming_response.add_file_to_graph( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - file_id="string", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + file_id="file_id", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -264,14 +264,14 @@ def test_streaming_response_add_file_to_graph(self, client: Writer) -> None: def test_path_params_add_file_to_graph(self, client: Writer) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `graph_id` but received ''"): client.graphs.with_raw_response.add_file_to_graph( - "", - file_id="string", + graph_id="", + file_id="file_id", ) @parametrize def test_method_remove_file_from_graph(self, client: Writer) -> None: graph = client.graphs.remove_file_from_graph( - "string", + file_id="file_id", graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert_matches_type(GraphRemoveFileFromGraphResponse, graph, path=["response"]) @@ -279,7 +279,7 @@ def test_method_remove_file_from_graph(self, client: Writer) -> None: @parametrize def test_raw_response_remove_file_from_graph(self, client: Writer) -> None: response = client.graphs.with_raw_response.remove_file_from_graph( - "string", + file_id="file_id", graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) @@ -291,7 +291,7 @@ def test_raw_response_remove_file_from_graph(self, client: Writer) -> None: @parametrize def test_streaming_response_remove_file_from_graph(self, client: Writer) -> None: with client.graphs.with_streaming_response.remove_file_from_graph( - "string", + file_id="file_id", graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: assert not response.is_closed @@ -306,13 +306,13 @@ def test_streaming_response_remove_file_from_graph(self, client: Writer) -> None def test_path_params_remove_file_from_graph(self, client: Writer) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `graph_id` but received ''"): client.graphs.with_raw_response.remove_file_from_graph( - "string", + file_id="file_id", graph_id="", ) with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): client.graphs.with_raw_response.remove_file_from_graph( - "", + file_id="", graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) @@ -323,22 +323,22 @@ class TestAsyncGraphs: @parametrize async def test_method_create(self, async_client: AsyncWriter) -> None: graph = await async_client.graphs.create( - name="string", + name="name", ) assert_matches_type(GraphCreateResponse, graph, path=["response"]) @parametrize async def test_method_create_with_all_params(self, async_client: AsyncWriter) -> None: graph = await async_client.graphs.create( - name="string", - description="string", + name="name", + description="description", ) assert_matches_type(GraphCreateResponse, graph, path=["response"]) @parametrize async def test_raw_response_create(self, async_client: AsyncWriter) -> None: response = await async_client.graphs.with_raw_response.create( - name="string", + name="name", ) assert response.is_closed is True @@ -349,7 +349,7 @@ async def test_raw_response_create(self, async_client: AsyncWriter) -> None: @parametrize async def test_streaming_response_create(self, async_client: AsyncWriter) -> None: async with async_client.graphs.with_streaming_response.create( - name="string", + name="name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -400,25 +400,25 @@ async def test_path_params_retrieve(self, async_client: AsyncWriter) -> None: @parametrize async def test_method_update(self, async_client: AsyncWriter) -> None: graph = await async_client.graphs.update( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - name="string", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", ) assert_matches_type(GraphUpdateResponse, graph, path=["response"]) @parametrize async def test_method_update_with_all_params(self, async_client: AsyncWriter) -> None: graph = await async_client.graphs.update( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - name="string", - description="string", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + description="description", ) assert_matches_type(GraphUpdateResponse, graph, path=["response"]) @parametrize async def test_raw_response_update(self, async_client: AsyncWriter) -> None: response = await async_client.graphs.with_raw_response.update( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - name="string", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", ) assert response.is_closed is True @@ -429,8 +429,8 @@ async def test_raw_response_update(self, async_client: AsyncWriter) -> None: @parametrize async def test_streaming_response_update(self, async_client: AsyncWriter) -> None: async with async_client.graphs.with_streaming_response.update( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - name="string", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -444,8 +444,8 @@ async def test_streaming_response_update(self, async_client: AsyncWriter) -> Non async def test_path_params_update(self, async_client: AsyncWriter) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `graph_id` but received ''"): await async_client.graphs.with_raw_response.update( - "", - name="string", + graph_id="", + name="name", ) @parametrize @@ -524,16 +524,16 @@ async def test_path_params_delete(self, async_client: AsyncWriter) -> None: @parametrize async def test_method_add_file_to_graph(self, async_client: AsyncWriter) -> None: graph = await async_client.graphs.add_file_to_graph( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - file_id="string", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + file_id="file_id", ) assert_matches_type(File, graph, path=["response"]) @parametrize async def test_raw_response_add_file_to_graph(self, async_client: AsyncWriter) -> None: response = await async_client.graphs.with_raw_response.add_file_to_graph( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - file_id="string", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + file_id="file_id", ) assert response.is_closed is True @@ -544,8 +544,8 @@ async def test_raw_response_add_file_to_graph(self, async_client: AsyncWriter) - @parametrize async def test_streaming_response_add_file_to_graph(self, async_client: AsyncWriter) -> None: async with async_client.graphs.with_streaming_response.add_file_to_graph( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - file_id="string", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + file_id="file_id", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -559,14 +559,14 @@ async def test_streaming_response_add_file_to_graph(self, async_client: AsyncWri async def test_path_params_add_file_to_graph(self, async_client: AsyncWriter) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `graph_id` but received ''"): await async_client.graphs.with_raw_response.add_file_to_graph( - "", - file_id="string", + graph_id="", + file_id="file_id", ) @parametrize async def test_method_remove_file_from_graph(self, async_client: AsyncWriter) -> None: graph = await async_client.graphs.remove_file_from_graph( - "string", + file_id="file_id", graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert_matches_type(GraphRemoveFileFromGraphResponse, graph, path=["response"]) @@ -574,7 +574,7 @@ async def test_method_remove_file_from_graph(self, async_client: AsyncWriter) -> @parametrize async def test_raw_response_remove_file_from_graph(self, async_client: AsyncWriter) -> None: response = await async_client.graphs.with_raw_response.remove_file_from_graph( - "string", + file_id="file_id", graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) @@ -586,7 +586,7 @@ async def test_raw_response_remove_file_from_graph(self, async_client: AsyncWrit @parametrize async def test_streaming_response_remove_file_from_graph(self, async_client: AsyncWriter) -> None: async with async_client.graphs.with_streaming_response.remove_file_from_graph( - "string", + file_id="file_id", graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: assert not response.is_closed @@ -601,12 +601,12 @@ async def test_streaming_response_remove_file_from_graph(self, async_client: Asy async def test_path_params_remove_file_from_graph(self, async_client: AsyncWriter) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `graph_id` but received ''"): await async_client.graphs.with_raw_response.remove_file_from_graph( - "string", + file_id="file_id", graph_id="", ) with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): await async_client.graphs.with_raw_response.remove_file_from_graph( - "", + file_id="", graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) diff --git a/tests/test_client.py b/tests/test_client.py index 4d863c8b..10b0faac 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -724,7 +724,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> No dict( messages=[ { - "content": "string", + "content": "content", "role": "user", } ], @@ -750,7 +750,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> Non dict( messages=[ { - "content": "string", + "content": "content", "role": "user", } ], @@ -1455,7 +1455,7 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) dict( messages=[ { - "content": "string", + "content": "content", "role": "user", } ], @@ -1481,7 +1481,7 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) dict( messages=[ { - "content": "string", + "content": "content", "role": "user", } ], From 553b2ddf5b0bd7ec2d95bd3b0d1da2cef6e74760 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 12 Jul 2024 08:15:42 +0000 Subject: [PATCH 035/399] chore(internal): version bump (#23) --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 10f30916..6b7b74c5 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.2.0" + ".": "0.3.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 43a9ace2..b6a61218 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "0.2.0" +version = "0.3.0" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index 11c5aafa..0ecdd281 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "0.2.0" # x-release-please-version +__version__ = "0.3.0" # x-release-please-version From f59487205caa174ecc0a37ff6584456f3ade7813 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 12 Jul 2024 11:10:58 +0000 Subject: [PATCH 036/399] feat(api): OpenAPI spec update via Stainless API (#25) --- .stats.yml | 4 +- api.md | 5 +- src/writerai/resources/files.py | 292 +++--------------- src/writerai/resources/graphs.py | 78 ++++- src/writerai/types/__init__.py | 1 - src/writerai/types/file.py | 4 + src/writerai/types/file_delete_response.py | 13 - src/writerai/types/file_list_params.py | 20 +- src/writerai/types/graph.py | 8 + .../types/graph_add_file_to_graph_params.py | 1 + src/writerai/types/graph_create_params.py | 2 + src/writerai/types/graph_create_response.py | 4 + src/writerai/types/graph_delete_response.py | 2 + src/writerai/types/graph_list_params.py | 16 + .../graph_remove_file_from_graph_response.py | 2 + src/writerai/types/graph_update_params.py | 2 + src/writerai/types/graph_update_response.py | 4 + tests/api_resources/test_files.py | 282 +---------------- 18 files changed, 191 insertions(+), 549 deletions(-) delete mode 100644 src/writerai/types/file_delete_response.py diff --git a/.stats.yml b/.stats.yml index e396c3c5..99e9dfc8 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ -configured_endpoints: 15 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-e54b835d1d64d37343017f4919c60998496a1b2b5c8e0c67af82093efc613f2f.yml +configured_endpoints: 12 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-eccf2e27a91b41489a70fdbe1253c1142da0f2f2d1e84267b037d6b6af0b26dd.yml diff --git a/api.md b/api.md index 9a27ad94..cf33548b 100644 --- a/api.md +++ b/api.md @@ -63,13 +63,10 @@ Methods: Types: ```python -from writerai.types import File, FileDeleteResponse +from writerai.types import File ``` Methods: -- client.files.retrieve(file_id) -> File - client.files.list(\*\*params) -> SyncCursorPage[File] -- client.files.delete(file_id) -> FileDeleteResponse -- client.files.download(file_id) -> BinaryAPIResponse - client.files.upload(\*\*params) -> File diff --git a/src/writerai/resources/files.py b/src/writerai/resources/files.py index 3ecec443..d30a6f4d 100644 --- a/src/writerai/resources/files.py +++ b/src/writerai/resources/files.py @@ -2,6 +2,7 @@ from __future__ import annotations +from typing import List from typing_extensions import Literal import httpx @@ -15,23 +16,14 @@ from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( - BinaryAPIResponse, - AsyncBinaryAPIResponse, - StreamedBinaryAPIResponse, - AsyncStreamedBinaryAPIResponse, to_raw_response_wrapper, to_streamed_response_wrapper, async_to_raw_response_wrapper, - to_custom_raw_response_wrapper, async_to_streamed_response_wrapper, - to_custom_streamed_response_wrapper, - async_to_custom_raw_response_wrapper, - async_to_custom_streamed_response_wrapper, ) from ..pagination import SyncCursorPage, AsyncCursorPage from ..types.file import File from .._base_client import AsyncPaginator, make_request_options -from ..types.file_delete_response import FileDeleteResponse __all__ = ["FilesResource", "AsyncFilesResource"] @@ -45,45 +37,12 @@ def with_raw_response(self) -> FilesResourceWithRawResponse: def with_streaming_response(self) -> FilesResourceWithStreamingResponse: return FilesResourceWithStreamingResponse(self) - def retrieve( - self, - file_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> File: - """ - Get metadata of a file - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not file_id: - raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") - return self._get( - f"/v1/files/{file_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=File, - ) - def list( self, *, after: str | NotGiven = NOT_GIVEN, before: str | NotGiven = NOT_GIVEN, - graph_id: str | NotGiven = NOT_GIVEN, + graph_id: List[str] | NotGiven = NOT_GIVEN, limit: int | NotGiven = NOT_GIVEN, order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -93,10 +52,25 @@ def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> SyncCursorPage[File]: - """ - Get metadata of all files + """List files Args: + after: The ID of the last object in the previous page. + + This parameter instructs the API + to return the next page of results. + + before: The ID of the first object in the previous page. This parameter instructs the + API to return the previous page of results. + + graph_id: The unique identifier of the graph to which the files belong. + + limit: Specifies the maximum number of objects returned in a page. The default value + is 50. The minimum value is 1, and the maximum value is 100. + + order: Specifies the order of the results. Valid values are asc for ascending and desc + for descending. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -127,73 +101,6 @@ def list( model=File, ) - def delete( - self, - file_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> FileDeleteResponse: - """ - Delete file - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not file_id: - raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") - return self._delete( - f"/v1/files/{file_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=FileDeleteResponse, - ) - - def download( - self, - file_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> BinaryAPIResponse: - """ - Download a file - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not file_id: - raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") - extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} - return self._get( - f"/v1/files/{file_id}/download", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=BinaryAPIResponse, - ) - def upload( self, *, @@ -245,45 +152,12 @@ def with_raw_response(self) -> AsyncFilesResourceWithRawResponse: def with_streaming_response(self) -> AsyncFilesResourceWithStreamingResponse: return AsyncFilesResourceWithStreamingResponse(self) - async def retrieve( - self, - file_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> File: - """ - Get metadata of a file - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not file_id: - raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") - return await self._get( - f"/v1/files/{file_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=File, - ) - def list( self, *, after: str | NotGiven = NOT_GIVEN, before: str | NotGiven = NOT_GIVEN, - graph_id: str | NotGiven = NOT_GIVEN, + graph_id: List[str] | NotGiven = NOT_GIVEN, limit: int | NotGiven = NOT_GIVEN, order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -293,10 +167,25 @@ def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> AsyncPaginator[File, AsyncCursorPage[File]]: - """ - Get metadata of all files + """List files Args: + after: The ID of the last object in the previous page. + + This parameter instructs the API + to return the next page of results. + + before: The ID of the first object in the previous page. This parameter instructs the + API to return the previous page of results. + + graph_id: The unique identifier of the graph to which the files belong. + + limit: Specifies the maximum number of objects returned in a page. The default value + is 50. The minimum value is 1, and the maximum value is 100. + + order: Specifies the order of the results. Valid values are asc for ascending and desc + for descending. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -327,73 +216,6 @@ def list( model=File, ) - async def delete( - self, - file_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> FileDeleteResponse: - """ - Delete file - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not file_id: - raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") - return await self._delete( - f"/v1/files/{file_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=FileDeleteResponse, - ) - - async def download( - self, - file_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AsyncBinaryAPIResponse: - """ - Download a file - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not file_id: - raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") - extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} - return await self._get( - f"/v1/files/{file_id}/download", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AsyncBinaryAPIResponse, - ) - async def upload( self, *, @@ -440,19 +262,9 @@ class FilesResourceWithRawResponse: def __init__(self, files: FilesResource) -> None: self._files = files - self.retrieve = to_raw_response_wrapper( - files.retrieve, - ) self.list = to_raw_response_wrapper( files.list, ) - self.delete = to_raw_response_wrapper( - files.delete, - ) - self.download = to_custom_raw_response_wrapper( - files.download, - BinaryAPIResponse, - ) self.upload = to_raw_response_wrapper( files.upload, ) @@ -462,19 +274,9 @@ class AsyncFilesResourceWithRawResponse: def __init__(self, files: AsyncFilesResource) -> None: self._files = files - self.retrieve = async_to_raw_response_wrapper( - files.retrieve, - ) self.list = async_to_raw_response_wrapper( files.list, ) - self.delete = async_to_raw_response_wrapper( - files.delete, - ) - self.download = async_to_custom_raw_response_wrapper( - files.download, - AsyncBinaryAPIResponse, - ) self.upload = async_to_raw_response_wrapper( files.upload, ) @@ -484,19 +286,9 @@ class FilesResourceWithStreamingResponse: def __init__(self, files: FilesResource) -> None: self._files = files - self.retrieve = to_streamed_response_wrapper( - files.retrieve, - ) self.list = to_streamed_response_wrapper( files.list, ) - self.delete = to_streamed_response_wrapper( - files.delete, - ) - self.download = to_custom_streamed_response_wrapper( - files.download, - StreamedBinaryAPIResponse, - ) self.upload = to_streamed_response_wrapper( files.upload, ) @@ -506,19 +298,9 @@ class AsyncFilesResourceWithStreamingResponse: def __init__(self, files: AsyncFilesResource) -> None: self._files = files - self.retrieve = async_to_streamed_response_wrapper( - files.retrieve, - ) self.list = async_to_streamed_response_wrapper( files.list, ) - self.delete = async_to_streamed_response_wrapper( - files.delete, - ) - self.download = async_to_custom_streamed_response_wrapper( - files.download, - AsyncStreamedBinaryAPIResponse, - ) self.upload = async_to_streamed_response_wrapper( files.upload, ) diff --git a/src/writerai/resources/graphs.py b/src/writerai/resources/graphs.py index f3f087cf..b31dac45 100644 --- a/src/writerai/resources/graphs.py +++ b/src/writerai/resources/graphs.py @@ -59,7 +59,13 @@ def create( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> GraphCreateResponse: """ + Create graph + Args: + name: The name of the graph. + + description: A description of the graph. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -95,6 +101,8 @@ def retrieve( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> Graph: """ + Retrieve graph + Args: extra_headers: Send extra headers @@ -128,7 +136,13 @@ def update( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> GraphUpdateResponse: """ + Update graph + Args: + name: The name of the graph. + + description: A description of the graph. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -168,8 +182,23 @@ def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> SyncCursorPage[Graph]: - """ + """List graphs + Args: + after: The ID of the last object in the previous page. + + This parameter instructs the API + to return the next page of results. + + before: The ID of the first object in the previous page. This parameter instructs the + API to return the previous page of results. + + limit: Specifies the maximum number of objects returned in a page. The default value + is 50. The minimum value is 1, and the maximum value is 100. + + order: Specifies the order of the results. Valid values are asc for ascending and desc + for descending. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -211,6 +240,8 @@ def delete( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> GraphDeleteResponse: """ + Delete graph + Args: extra_headers: Send extra headers @@ -243,7 +274,11 @@ def add_file_to_graph( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> File: """ + Add file to graph + Args: + file_id: The unique identifier of the file. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -276,6 +311,8 @@ def remove_file_from_graph( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> GraphRemoveFileFromGraphResponse: """ + Remove file from graph + Args: extra_headers: Send extra headers @@ -320,7 +357,13 @@ async def create( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> GraphCreateResponse: """ + Create graph + Args: + name: The name of the graph. + + description: A description of the graph. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -356,6 +399,8 @@ async def retrieve( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> Graph: """ + Retrieve graph + Args: extra_headers: Send extra headers @@ -389,7 +434,13 @@ async def update( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> GraphUpdateResponse: """ + Update graph + Args: + name: The name of the graph. + + description: A description of the graph. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -429,8 +480,23 @@ def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> AsyncPaginator[Graph, AsyncCursorPage[Graph]]: - """ + """List graphs + Args: + after: The ID of the last object in the previous page. + + This parameter instructs the API + to return the next page of results. + + before: The ID of the first object in the previous page. This parameter instructs the + API to return the previous page of results. + + limit: Specifies the maximum number of objects returned in a page. The default value + is 50. The minimum value is 1, and the maximum value is 100. + + order: Specifies the order of the results. Valid values are asc for ascending and desc + for descending. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -472,6 +538,8 @@ async def delete( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> GraphDeleteResponse: """ + Delete graph + Args: extra_headers: Send extra headers @@ -504,7 +572,11 @@ async def add_file_to_graph( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> File: """ + Add file to graph + Args: + file_id: The unique identifier of the file. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -539,6 +611,8 @@ async def remove_file_from_graph( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> GraphRemoveFileFromGraphResponse: """ + Remove file from graph + Args: extra_headers: Send extra headers diff --git a/src/writerai/types/__init__.py b/src/writerai/types/__init__.py index 0e549136..b2e31c1b 100644 --- a/src/writerai/types/__init__.py +++ b/src/writerai/types/__init__.py @@ -15,7 +15,6 @@ from .graph_create_params import GraphCreateParams as GraphCreateParams from .graph_update_params import GraphUpdateParams as GraphUpdateParams from .model_list_response import ModelListResponse as ModelListResponse -from .file_delete_response import FileDeleteResponse as FileDeleteResponse from .graph_create_response import GraphCreateResponse as GraphCreateResponse from .graph_delete_response import GraphDeleteResponse as GraphDeleteResponse from .graph_update_response import GraphUpdateResponse as GraphUpdateResponse diff --git a/src/writerai/types/file.py b/src/writerai/types/file.py index 9448c403..2f8d5630 100644 --- a/src/writerai/types/file.py +++ b/src/writerai/types/file.py @@ -10,9 +10,13 @@ class File(BaseModel): id: str + """A unique identifier of the graph.""" created_at: datetime + """The timestamp when the graph was created.""" graph_ids: List[str] + """A list of graph IDs that the file is associated with.""" name: str + """The name of the graph.""" diff --git a/src/writerai/types/file_delete_response.py b/src/writerai/types/file_delete_response.py deleted file mode 100644 index 450d558c..00000000 --- a/src/writerai/types/file_delete_response.py +++ /dev/null @@ -1,13 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - - - -from .._models import BaseModel - -__all__ = ["FileDeleteResponse"] - - -class FileDeleteResponse(BaseModel): - id: str - - deleted: bool diff --git a/src/writerai/types/file_list_params.py b/src/writerai/types/file_list_params.py index 60866952..a890be74 100644 --- a/src/writerai/types/file_list_params.py +++ b/src/writerai/types/file_list_params.py @@ -2,6 +2,7 @@ from __future__ import annotations +from typing import List from typing_extensions import Literal, TypedDict __all__ = ["FileListParams"] @@ -9,11 +10,28 @@ class FileListParams(TypedDict, total=False): after: str + """The ID of the last object in the previous page. + + This parameter instructs the API to return the next page of results. + """ before: str + """The ID of the first object in the previous page. + + This parameter instructs the API to return the previous page of results. + """ - graph_id: str + graph_id: List[str] + """The unique identifier of the graph to which the files belong.""" limit: int + """Specifies the maximum number of objects returned in a page. + + The default value is 50. The minimum value is 1, and the maximum value is 100. + """ order: Literal["asc", "desc"] + """Specifies the order of the results. + + Valid values are asc for ascending and desc for descending. + """ diff --git a/src/writerai/types/graph.py b/src/writerai/types/graph.py index 2045a5d0..27816f02 100644 --- a/src/writerai/types/graph.py +++ b/src/writerai/types/graph.py @@ -10,21 +10,29 @@ class FileStatus(BaseModel): completed: int + """The number of files that have been successfully processed.""" failed: int + """The number of files that failed to process.""" in_progress: int + """The number of files currently being processed.""" total: int + """The total number of files associated with the graph.""" class Graph(BaseModel): id: str + """A unique identifier of the file.""" created_at: datetime + """The timestamp when the file was created.""" file_status: FileStatus name: str + """The name of the file.""" description: Optional[str] = None + """A description of the graph.""" diff --git a/src/writerai/types/graph_add_file_to_graph_params.py b/src/writerai/types/graph_add_file_to_graph_params.py index b1bab49a..b0c471d0 100644 --- a/src/writerai/types/graph_add_file_to_graph_params.py +++ b/src/writerai/types/graph_add_file_to_graph_params.py @@ -9,3 +9,4 @@ class GraphAddFileToGraphParams(TypedDict, total=False): file_id: Required[str] + """The unique identifier of the file.""" diff --git a/src/writerai/types/graph_create_params.py b/src/writerai/types/graph_create_params.py index 24638cf3..8f1dd274 100644 --- a/src/writerai/types/graph_create_params.py +++ b/src/writerai/types/graph_create_params.py @@ -9,5 +9,7 @@ class GraphCreateParams(TypedDict, total=False): name: Required[str] + """The name of the graph.""" description: str + """A description of the graph.""" diff --git a/src/writerai/types/graph_create_response.py b/src/writerai/types/graph_create_response.py index 831d2982..408e77c1 100644 --- a/src/writerai/types/graph_create_response.py +++ b/src/writerai/types/graph_create_response.py @@ -10,9 +10,13 @@ class GraphCreateResponse(BaseModel): id: str + """A unique identifier of the graph.""" created_at: datetime + """The timestamp when the graph was created.""" name: str + """The name of the graph.""" description: Optional[str] = None + """A description of the graph.""" diff --git a/src/writerai/types/graph_delete_response.py b/src/writerai/types/graph_delete_response.py index e689b600..f5aff6b3 100644 --- a/src/writerai/types/graph_delete_response.py +++ b/src/writerai/types/graph_delete_response.py @@ -9,5 +9,7 @@ class GraphDeleteResponse(BaseModel): id: str + """A unique identifier of the deleted file.""" deleted: bool + """Indicates whether the file was successfully deleted.""" diff --git a/src/writerai/types/graph_list_params.py b/src/writerai/types/graph_list_params.py index 37668efc..d83fcc33 100644 --- a/src/writerai/types/graph_list_params.py +++ b/src/writerai/types/graph_list_params.py @@ -9,9 +9,25 @@ class GraphListParams(TypedDict, total=False): after: str + """The ID of the last object in the previous page. + + This parameter instructs the API to return the next page of results. + """ before: str + """The ID of the first object in the previous page. + + This parameter instructs the API to return the previous page of results. + """ limit: int + """Specifies the maximum number of objects returned in a page. + + The default value is 50. The minimum value is 1, and the maximum value is 100. + """ order: Literal["asc", "desc"] + """Specifies the order of the results. + + Valid values are asc for ascending and desc for descending. + """ diff --git a/src/writerai/types/graph_remove_file_from_graph_response.py b/src/writerai/types/graph_remove_file_from_graph_response.py index bcf6d726..3adc8c23 100644 --- a/src/writerai/types/graph_remove_file_from_graph_response.py +++ b/src/writerai/types/graph_remove_file_from_graph_response.py @@ -9,5 +9,7 @@ class GraphRemoveFileFromGraphResponse(BaseModel): id: str + """A unique identifier of the deleted graph.""" deleted: bool + """Indicates whether the graph was successfully deleted.""" diff --git a/src/writerai/types/graph_update_params.py b/src/writerai/types/graph_update_params.py index 06ef5a98..b0d734a4 100644 --- a/src/writerai/types/graph_update_params.py +++ b/src/writerai/types/graph_update_params.py @@ -9,5 +9,7 @@ class GraphUpdateParams(TypedDict, total=False): name: Required[str] + """The name of the graph.""" description: str + """A description of the graph.""" diff --git a/src/writerai/types/graph_update_response.py b/src/writerai/types/graph_update_response.py index 63a3f421..fcc7825f 100644 --- a/src/writerai/types/graph_update_response.py +++ b/src/writerai/types/graph_update_response.py @@ -10,9 +10,13 @@ class GraphUpdateResponse(BaseModel): id: str + """A unique identifier of the graph.""" created_at: datetime + """The timestamp when the graph was created.""" name: str + """The name of the graph.""" description: Optional[str] = None + """A description of the graph.""" diff --git a/tests/api_resources/test_files.py b/tests/api_resources/test_files.py index aaa93d8d..e947e981 100644 --- a/tests/api_resources/test_files.py +++ b/tests/api_resources/test_files.py @@ -5,19 +5,11 @@ import os from typing import Any, cast -import httpx import pytest -from respx import MockRouter from writerai import Writer, AsyncWriter from tests.utils import assert_matches_type -from writerai.types import File, FileDeleteResponse -from writerai._response import ( - BinaryAPIResponse, - AsyncBinaryAPIResponse, - StreamedBinaryAPIResponse, - AsyncStreamedBinaryAPIResponse, -) +from writerai.types import File from writerai.pagination import SyncCursorPage, AsyncCursorPage base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -26,44 +18,6 @@ class TestFiles: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @parametrize - def test_method_retrieve(self, client: Writer) -> None: - file = client.files.retrieve( - "fileId", - ) - assert_matches_type(File, file, path=["response"]) - - @parametrize - def test_raw_response_retrieve(self, client: Writer) -> None: - response = client.files.with_raw_response.retrieve( - "fileId", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - file = response.parse() - assert_matches_type(File, file, path=["response"]) - - @parametrize - def test_streaming_response_retrieve(self, client: Writer) -> None: - with client.files.with_streaming_response.retrieve( - "fileId", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - file = response.parse() - assert_matches_type(File, file, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @parametrize - def test_path_params_retrieve(self, client: Writer) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): - client.files.with_raw_response.retrieve( - "", - ) - @parametrize def test_method_list(self, client: Writer) -> None: file = client.files.list() @@ -74,7 +28,11 @@ def test_method_list_with_all_params(self, client: Writer) -> None: file = client.files.list( after="after", before="before", - graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + graph_id=[ + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ], limit=0, order="asc", ) @@ -100,98 +58,6 @@ def test_streaming_response_list(self, client: Writer) -> None: assert cast(Any, response.is_closed) is True - @parametrize - def test_method_delete(self, client: Writer) -> None: - file = client.files.delete( - "fileId", - ) - assert_matches_type(FileDeleteResponse, file, path=["response"]) - - @parametrize - def test_raw_response_delete(self, client: Writer) -> None: - response = client.files.with_raw_response.delete( - "fileId", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - file = response.parse() - assert_matches_type(FileDeleteResponse, file, path=["response"]) - - @parametrize - def test_streaming_response_delete(self, client: Writer) -> None: - with client.files.with_streaming_response.delete( - "fileId", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - file = response.parse() - assert_matches_type(FileDeleteResponse, file, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @parametrize - def test_path_params_delete(self, client: Writer) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): - client.files.with_raw_response.delete( - "", - ) - - @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") - @parametrize - @pytest.mark.respx(base_url=base_url) - def test_method_download(self, client: Writer, respx_mock: MockRouter) -> None: - respx_mock.get("/v1/files/fileId/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - file = client.files.download( - "fileId", - ) - assert file.is_closed - assert file.json() == {"foo": "bar"} - assert cast(Any, file.is_closed) is True - assert isinstance(file, BinaryAPIResponse) - - @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") - @parametrize - @pytest.mark.respx(base_url=base_url) - def test_raw_response_download(self, client: Writer, respx_mock: MockRouter) -> None: - respx_mock.get("/v1/files/fileId/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - - file = client.files.with_raw_response.download( - "fileId", - ) - - assert file.is_closed is True - assert file.http_request.headers.get("X-Stainless-Lang") == "python" - assert file.json() == {"foo": "bar"} - assert isinstance(file, BinaryAPIResponse) - - @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") - @parametrize - @pytest.mark.respx(base_url=base_url) - def test_streaming_response_download(self, client: Writer, respx_mock: MockRouter) -> None: - respx_mock.get("/v1/files/fileId/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - with client.files.with_streaming_response.download( - "fileId", - ) as file: - assert not file.is_closed - assert file.http_request.headers.get("X-Stainless-Lang") == "python" - - assert file.json() == {"foo": "bar"} - assert cast(Any, file.is_closed) is True - assert isinstance(file, StreamedBinaryAPIResponse) - - assert cast(Any, file.is_closed) is True - - @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") - @parametrize - @pytest.mark.respx(base_url=base_url) - def test_path_params_download(self, client: Writer) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): - client.files.with_raw_response.download( - "", - ) - @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") @parametrize def test_method_upload(self, client: Writer) -> None: @@ -239,44 +105,6 @@ def test_streaming_response_upload(self, client: Writer) -> None: class TestAsyncFiles: parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) - @parametrize - async def test_method_retrieve(self, async_client: AsyncWriter) -> None: - file = await async_client.files.retrieve( - "fileId", - ) - assert_matches_type(File, file, path=["response"]) - - @parametrize - async def test_raw_response_retrieve(self, async_client: AsyncWriter) -> None: - response = await async_client.files.with_raw_response.retrieve( - "fileId", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - file = await response.parse() - assert_matches_type(File, file, path=["response"]) - - @parametrize - async def test_streaming_response_retrieve(self, async_client: AsyncWriter) -> None: - async with async_client.files.with_streaming_response.retrieve( - "fileId", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - file = await response.parse() - assert_matches_type(File, file, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @parametrize - async def test_path_params_retrieve(self, async_client: AsyncWriter) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): - await async_client.files.with_raw_response.retrieve( - "", - ) - @parametrize async def test_method_list(self, async_client: AsyncWriter) -> None: file = await async_client.files.list() @@ -287,7 +115,11 @@ async def test_method_list_with_all_params(self, async_client: AsyncWriter) -> N file = await async_client.files.list( after="after", before="before", - graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + graph_id=[ + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ], limit=0, order="asc", ) @@ -313,98 +145,6 @@ async def test_streaming_response_list(self, async_client: AsyncWriter) -> None: assert cast(Any, response.is_closed) is True - @parametrize - async def test_method_delete(self, async_client: AsyncWriter) -> None: - file = await async_client.files.delete( - "fileId", - ) - assert_matches_type(FileDeleteResponse, file, path=["response"]) - - @parametrize - async def test_raw_response_delete(self, async_client: AsyncWriter) -> None: - response = await async_client.files.with_raw_response.delete( - "fileId", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - file = await response.parse() - assert_matches_type(FileDeleteResponse, file, path=["response"]) - - @parametrize - async def test_streaming_response_delete(self, async_client: AsyncWriter) -> None: - async with async_client.files.with_streaming_response.delete( - "fileId", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - file = await response.parse() - assert_matches_type(FileDeleteResponse, file, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @parametrize - async def test_path_params_delete(self, async_client: AsyncWriter) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): - await async_client.files.with_raw_response.delete( - "", - ) - - @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") - @parametrize - @pytest.mark.respx(base_url=base_url) - async def test_method_download(self, async_client: AsyncWriter, respx_mock: MockRouter) -> None: - respx_mock.get("/v1/files/fileId/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - file = await async_client.files.download( - "fileId", - ) - assert file.is_closed - assert await file.json() == {"foo": "bar"} - assert cast(Any, file.is_closed) is True - assert isinstance(file, AsyncBinaryAPIResponse) - - @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") - @parametrize - @pytest.mark.respx(base_url=base_url) - async def test_raw_response_download(self, async_client: AsyncWriter, respx_mock: MockRouter) -> None: - respx_mock.get("/v1/files/fileId/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - - file = await async_client.files.with_raw_response.download( - "fileId", - ) - - assert file.is_closed is True - assert file.http_request.headers.get("X-Stainless-Lang") == "python" - assert await file.json() == {"foo": "bar"} - assert isinstance(file, AsyncBinaryAPIResponse) - - @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") - @parametrize - @pytest.mark.respx(base_url=base_url) - async def test_streaming_response_download(self, async_client: AsyncWriter, respx_mock: MockRouter) -> None: - respx_mock.get("/v1/files/fileId/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - async with async_client.files.with_streaming_response.download( - "fileId", - ) as file: - assert not file.is_closed - assert file.http_request.headers.get("X-Stainless-Lang") == "python" - - assert await file.json() == {"foo": "bar"} - assert cast(Any, file.is_closed) is True - assert isinstance(file, AsyncStreamedBinaryAPIResponse) - - assert cast(Any, file.is_closed) is True - - @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") - @parametrize - @pytest.mark.respx(base_url=base_url) - async def test_path_params_download(self, async_client: AsyncWriter) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): - await async_client.files.with_raw_response.download( - "", - ) - @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") @parametrize async def test_method_upload(self, async_client: AsyncWriter) -> None: From d8a5a402afea370785b2a91c60aa287f132aafb9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 12 Jul 2024 12:58:03 +0000 Subject: [PATCH 037/399] feat(api): update via SDK Studio (#26) --- .stats.yml | 2 +- tests/api_resources/test_chat.py | 32 ++++++++++++++++---------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.stats.yml b/.stats.yml index 99e9dfc8..3273a31e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 12 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-eccf2e27a91b41489a70fdbe1253c1142da0f2f2d1e84267b037d6b6af0b26dd.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-ca8da7974057afac39ab3f38dbd707d4db9ea11fa15d95061c82642ca4c79ef8.yml diff --git a/tests/api_resources/test_chat.py b/tests/api_resources/test_chat.py index 5789a046..1d74a3b3 100644 --- a/tests/api_resources/test_chat.py +++ b/tests/api_resources/test_chat.py @@ -26,7 +26,7 @@ def test_method_chat_overload_1(self, client: Writer) -> None: "role": "user", } ], - model="palmyra-x-002-32k", + model="model", ) assert_matches_type(Chat, chat, path=["response"]) @@ -40,7 +40,7 @@ def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: "name": "name", } ], - model="palmyra-x-002-32k", + model="model", max_tokens=0, n=0, stop=["string", "string", "string"], @@ -59,7 +59,7 @@ def test_raw_response_chat_overload_1(self, client: Writer) -> None: "role": "user", } ], - model="palmyra-x-002-32k", + model="model", ) assert response.is_closed is True @@ -76,7 +76,7 @@ def test_streaming_response_chat_overload_1(self, client: Writer) -> None: "role": "user", } ], - model="palmyra-x-002-32k", + model="model", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -95,7 +95,7 @@ def test_method_chat_overload_2(self, client: Writer) -> None: "role": "user", } ], - model="palmyra-x-002-32k", + model="model", stream=True, ) chat_stream.response.close() @@ -110,7 +110,7 @@ def test_method_chat_with_all_params_overload_2(self, client: Writer) -> None: "name": "name", } ], - model="palmyra-x-002-32k", + model="model", stream=True, max_tokens=0, n=0, @@ -129,7 +129,7 @@ def test_raw_response_chat_overload_2(self, client: Writer) -> None: "role": "user", } ], - model="palmyra-x-002-32k", + model="model", stream=True, ) @@ -146,7 +146,7 @@ def test_streaming_response_chat_overload_2(self, client: Writer) -> None: "role": "user", } ], - model="palmyra-x-002-32k", + model="model", stream=True, ) as response: assert not response.is_closed @@ -170,7 +170,7 @@ async def test_method_chat_overload_1(self, async_client: AsyncWriter) -> None: "role": "user", } ], - model="palmyra-x-002-32k", + model="model", ) assert_matches_type(Chat, chat, path=["response"]) @@ -184,7 +184,7 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW "name": "name", } ], - model="palmyra-x-002-32k", + model="model", max_tokens=0, n=0, stop=["string", "string", "string"], @@ -203,7 +203,7 @@ async def test_raw_response_chat_overload_1(self, async_client: AsyncWriter) -> "role": "user", } ], - model="palmyra-x-002-32k", + model="model", ) assert response.is_closed is True @@ -220,7 +220,7 @@ async def test_streaming_response_chat_overload_1(self, async_client: AsyncWrite "role": "user", } ], - model="palmyra-x-002-32k", + model="model", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -239,7 +239,7 @@ async def test_method_chat_overload_2(self, async_client: AsyncWriter) -> None: "role": "user", } ], - model="palmyra-x-002-32k", + model="model", stream=True, ) await chat_stream.response.aclose() @@ -254,7 +254,7 @@ async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncW "name": "name", } ], - model="palmyra-x-002-32k", + model="model", stream=True, max_tokens=0, n=0, @@ -273,7 +273,7 @@ async def test_raw_response_chat_overload_2(self, async_client: AsyncWriter) -> "role": "user", } ], - model="palmyra-x-002-32k", + model="model", stream=True, ) @@ -290,7 +290,7 @@ async def test_streaming_response_chat_overload_2(self, async_client: AsyncWrite "role": "user", } ], - model="palmyra-x-002-32k", + model="model", stream=True, ) as response: assert not response.is_closed From eb4e15cd1cadbdc9eb21d9b5556b1ca73b0b6f9c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 12 Jul 2024 14:44:26 +0000 Subject: [PATCH 038/399] feat(api): update via SDK Studio (#27) --- .stats.yml | 2 +- src/writerai/resources/files.py | 5 ++--- src/writerai/types/file_list_params.py | 3 +-- tests/api_resources/test_files.py | 12 ++---------- 4 files changed, 6 insertions(+), 16 deletions(-) diff --git a/.stats.yml b/.stats.yml index 3273a31e..a85d0c3f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 12 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-ca8da7974057afac39ab3f38dbd707d4db9ea11fa15d95061c82642ca4c79ef8.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-5ec9075c9a60b855ae0ac9907a6c2a4c6216fb2087d8d47125aec013ebc5f3d5.yml diff --git a/src/writerai/resources/files.py b/src/writerai/resources/files.py index d30a6f4d..c1e7e78a 100644 --- a/src/writerai/resources/files.py +++ b/src/writerai/resources/files.py @@ -2,7 +2,6 @@ from __future__ import annotations -from typing import List from typing_extensions import Literal import httpx @@ -42,7 +41,7 @@ def list( *, after: str | NotGiven = NOT_GIVEN, before: str | NotGiven = NOT_GIVEN, - graph_id: List[str] | NotGiven = NOT_GIVEN, + graph_id: str | NotGiven = NOT_GIVEN, limit: int | NotGiven = NOT_GIVEN, order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -157,7 +156,7 @@ def list( *, after: str | NotGiven = NOT_GIVEN, before: str | NotGiven = NOT_GIVEN, - graph_id: List[str] | NotGiven = NOT_GIVEN, + graph_id: str | NotGiven = NOT_GIVEN, limit: int | NotGiven = NOT_GIVEN, order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. diff --git a/src/writerai/types/file_list_params.py b/src/writerai/types/file_list_params.py index a890be74..1a803015 100644 --- a/src/writerai/types/file_list_params.py +++ b/src/writerai/types/file_list_params.py @@ -2,7 +2,6 @@ from __future__ import annotations -from typing import List from typing_extensions import Literal, TypedDict __all__ = ["FileListParams"] @@ -21,7 +20,7 @@ class FileListParams(TypedDict, total=False): This parameter instructs the API to return the previous page of results. """ - graph_id: List[str] + graph_id: str """The unique identifier of the graph to which the files belong.""" limit: int diff --git a/tests/api_resources/test_files.py b/tests/api_resources/test_files.py index e947e981..925cde45 100644 --- a/tests/api_resources/test_files.py +++ b/tests/api_resources/test_files.py @@ -28,11 +28,7 @@ def test_method_list_with_all_params(self, client: Writer) -> None: file = client.files.list( after="after", before="before", - graph_id=[ - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ], + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", limit=0, order="asc", ) @@ -115,11 +111,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncWriter) -> N file = await async_client.files.list( after="after", before="before", - graph_id=[ - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ], + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", limit=0, order="asc", ) From 620596852b24513568a59cab5df0b336765a9846 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 12 Jul 2024 14:47:40 +0000 Subject: [PATCH 039/399] chore(internal): version bump (#28) --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 6b7b74c5..da59f99e 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.3.0" + ".": "0.4.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index b6a61218..31bdbc80 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "0.3.0" +version = "0.4.0" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index 0ecdd281..6af8a6b2 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "0.3.0" # x-release-please-version +__version__ = "0.4.0" # x-release-please-version From 90e8984f58c98d51e25e78723be71632a181f270 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 12 Jul 2024 15:00:38 +0000 Subject: [PATCH 040/399] feat(api): OpenAPI spec update via Stainless API (#29) --- .stats.yml | 2 +- src/writerai/resources/files.py | 5 +++-- src/writerai/types/file_list_params.py | 3 ++- tests/api_resources/test_files.py | 12 ++++++++++-- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/.stats.yml b/.stats.yml index a85d0c3f..3273a31e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 12 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-5ec9075c9a60b855ae0ac9907a6c2a4c6216fb2087d8d47125aec013ebc5f3d5.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-ca8da7974057afac39ab3f38dbd707d4db9ea11fa15d95061c82642ca4c79ef8.yml diff --git a/src/writerai/resources/files.py b/src/writerai/resources/files.py index c1e7e78a..d30a6f4d 100644 --- a/src/writerai/resources/files.py +++ b/src/writerai/resources/files.py @@ -2,6 +2,7 @@ from __future__ import annotations +from typing import List from typing_extensions import Literal import httpx @@ -41,7 +42,7 @@ def list( *, after: str | NotGiven = NOT_GIVEN, before: str | NotGiven = NOT_GIVEN, - graph_id: str | NotGiven = NOT_GIVEN, + graph_id: List[str] | NotGiven = NOT_GIVEN, limit: int | NotGiven = NOT_GIVEN, order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -156,7 +157,7 @@ def list( *, after: str | NotGiven = NOT_GIVEN, before: str | NotGiven = NOT_GIVEN, - graph_id: str | NotGiven = NOT_GIVEN, + graph_id: List[str] | NotGiven = NOT_GIVEN, limit: int | NotGiven = NOT_GIVEN, order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. diff --git a/src/writerai/types/file_list_params.py b/src/writerai/types/file_list_params.py index 1a803015..a890be74 100644 --- a/src/writerai/types/file_list_params.py +++ b/src/writerai/types/file_list_params.py @@ -2,6 +2,7 @@ from __future__ import annotations +from typing import List from typing_extensions import Literal, TypedDict __all__ = ["FileListParams"] @@ -20,7 +21,7 @@ class FileListParams(TypedDict, total=False): This parameter instructs the API to return the previous page of results. """ - graph_id: str + graph_id: List[str] """The unique identifier of the graph to which the files belong.""" limit: int diff --git a/tests/api_resources/test_files.py b/tests/api_resources/test_files.py index 925cde45..e947e981 100644 --- a/tests/api_resources/test_files.py +++ b/tests/api_resources/test_files.py @@ -28,7 +28,11 @@ def test_method_list_with_all_params(self, client: Writer) -> None: file = client.files.list( after="after", before="before", - graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + graph_id=[ + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ], limit=0, order="asc", ) @@ -111,7 +115,11 @@ async def test_method_list_with_all_params(self, async_client: AsyncWriter) -> N file = await async_client.files.list( after="after", before="before", - graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + graph_id=[ + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ], limit=0, order="asc", ) From ce569095281fb410f9cf524cddbf616ffd969ab6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 14 Jul 2024 11:42:21 +0000 Subject: [PATCH 041/399] feat(api): OpenAPI spec update via Stainless API (#31) --- .stats.yml | 2 +- src/writerai/resources/files.py | 5 ++--- src/writerai/types/file_list_params.py | 3 +-- tests/api_resources/test_files.py | 12 ++---------- 4 files changed, 6 insertions(+), 16 deletions(-) diff --git a/.stats.yml b/.stats.yml index 3273a31e..b2a2ac22 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 12 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-ca8da7974057afac39ab3f38dbd707d4db9ea11fa15d95061c82642ca4c79ef8.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-2aa0ec0ea99873da5dac6ecf610efdd6ccfe5601d2400f8c475d994cd47c2c36.yml diff --git a/src/writerai/resources/files.py b/src/writerai/resources/files.py index d30a6f4d..c1e7e78a 100644 --- a/src/writerai/resources/files.py +++ b/src/writerai/resources/files.py @@ -2,7 +2,6 @@ from __future__ import annotations -from typing import List from typing_extensions import Literal import httpx @@ -42,7 +41,7 @@ def list( *, after: str | NotGiven = NOT_GIVEN, before: str | NotGiven = NOT_GIVEN, - graph_id: List[str] | NotGiven = NOT_GIVEN, + graph_id: str | NotGiven = NOT_GIVEN, limit: int | NotGiven = NOT_GIVEN, order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -157,7 +156,7 @@ def list( *, after: str | NotGiven = NOT_GIVEN, before: str | NotGiven = NOT_GIVEN, - graph_id: List[str] | NotGiven = NOT_GIVEN, + graph_id: str | NotGiven = NOT_GIVEN, limit: int | NotGiven = NOT_GIVEN, order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. diff --git a/src/writerai/types/file_list_params.py b/src/writerai/types/file_list_params.py index a890be74..1a803015 100644 --- a/src/writerai/types/file_list_params.py +++ b/src/writerai/types/file_list_params.py @@ -2,7 +2,6 @@ from __future__ import annotations -from typing import List from typing_extensions import Literal, TypedDict __all__ = ["FileListParams"] @@ -21,7 +20,7 @@ class FileListParams(TypedDict, total=False): This parameter instructs the API to return the previous page of results. """ - graph_id: List[str] + graph_id: str """The unique identifier of the graph to which the files belong.""" limit: int diff --git a/tests/api_resources/test_files.py b/tests/api_resources/test_files.py index e947e981..925cde45 100644 --- a/tests/api_resources/test_files.py +++ b/tests/api_resources/test_files.py @@ -28,11 +28,7 @@ def test_method_list_with_all_params(self, client: Writer) -> None: file = client.files.list( after="after", before="before", - graph_id=[ - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ], + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", limit=0, order="asc", ) @@ -115,11 +111,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncWriter) -> N file = await async_client.files.list( after="after", before="before", - graph_id=[ - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ], + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", limit=0, order="asc", ) From 9d96181121b802802d2e9dd147e77cbf18136488 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 14 Jul 2024 14:02:43 +0000 Subject: [PATCH 042/399] feat(api): update via SDK Studio (#32) --- .stats.yml | 2 +- api.md | 5 +- src/writerai/resources/files.py | 249 +++++++++++++++++++ src/writerai/types/__init__.py | 1 + src/writerai/types/file_delete_response.py | 15 ++ tests/api_resources/test_files.py | 270 ++++++++++++++++++++- 6 files changed, 539 insertions(+), 3 deletions(-) create mode 100644 src/writerai/types/file_delete_response.py diff --git a/.stats.yml b/.stats.yml index b2a2ac22..468de8f6 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ -configured_endpoints: 12 +configured_endpoints: 15 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-2aa0ec0ea99873da5dac6ecf610efdd6ccfe5601d2400f8c475d994cd47c2c36.yml diff --git a/api.md b/api.md index cf33548b..abecbd8a 100644 --- a/api.md +++ b/api.md @@ -63,10 +63,13 @@ Methods: Types: ```python -from writerai.types import File +from writerai.types import File, FileDeleteResponse ``` Methods: +- client.files.retrieve(file_id) -> File - client.files.list(\*\*params) -> SyncCursorPage[File] +- client.files.delete(file_id) -> FileDeleteResponse +- client.files.download(file_id) -> BinaryAPIResponse - client.files.upload(\*\*params) -> File diff --git a/src/writerai/resources/files.py b/src/writerai/resources/files.py index c1e7e78a..11df91bc 100644 --- a/src/writerai/resources/files.py +++ b/src/writerai/resources/files.py @@ -15,14 +15,23 @@ from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( + BinaryAPIResponse, + AsyncBinaryAPIResponse, + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, to_raw_response_wrapper, to_streamed_response_wrapper, async_to_raw_response_wrapper, + to_custom_raw_response_wrapper, async_to_streamed_response_wrapper, + to_custom_streamed_response_wrapper, + async_to_custom_raw_response_wrapper, + async_to_custom_streamed_response_wrapper, ) from ..pagination import SyncCursorPage, AsyncCursorPage from ..types.file import File from .._base_client import AsyncPaginator, make_request_options +from ..types.file_delete_response import FileDeleteResponse __all__ = ["FilesResource", "AsyncFilesResource"] @@ -36,6 +45,39 @@ def with_raw_response(self) -> FilesResourceWithRawResponse: def with_streaming_response(self) -> FilesResourceWithStreamingResponse: return FilesResourceWithStreamingResponse(self) + def retrieve( + self, + file_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> File: + """ + Retrieve file + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + return self._get( + f"/v1/files/{file_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=File, + ) + def list( self, *, @@ -100,6 +142,73 @@ def list( model=File, ) + def delete( + self, + file_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> FileDeleteResponse: + """ + Delete file + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + return self._delete( + f"/v1/files/{file_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=FileDeleteResponse, + ) + + def download( + self, + file_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BinaryAPIResponse: + """ + Download file + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + extra_headers = {"Accept": "file contents", **(extra_headers or {})} + return self._get( + f"/v1/files/{file_id}/download", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BinaryAPIResponse, + ) + def upload( self, *, @@ -151,6 +260,39 @@ def with_raw_response(self) -> AsyncFilesResourceWithRawResponse: def with_streaming_response(self) -> AsyncFilesResourceWithStreamingResponse: return AsyncFilesResourceWithStreamingResponse(self) + async def retrieve( + self, + file_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> File: + """ + Retrieve file + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + return await self._get( + f"/v1/files/{file_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=File, + ) + def list( self, *, @@ -215,6 +357,73 @@ def list( model=File, ) + async def delete( + self, + file_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> FileDeleteResponse: + """ + Delete file + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + return await self._delete( + f"/v1/files/{file_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=FileDeleteResponse, + ) + + async def download( + self, + file_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncBinaryAPIResponse: + """ + Download file + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + extra_headers = {"Accept": "file contents", **(extra_headers or {})} + return await self._get( + f"/v1/files/{file_id}/download", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AsyncBinaryAPIResponse, + ) + async def upload( self, *, @@ -261,9 +470,19 @@ class FilesResourceWithRawResponse: def __init__(self, files: FilesResource) -> None: self._files = files + self.retrieve = to_raw_response_wrapper( + files.retrieve, + ) self.list = to_raw_response_wrapper( files.list, ) + self.delete = to_raw_response_wrapper( + files.delete, + ) + self.download = to_custom_raw_response_wrapper( + files.download, + BinaryAPIResponse, + ) self.upload = to_raw_response_wrapper( files.upload, ) @@ -273,9 +492,19 @@ class AsyncFilesResourceWithRawResponse: def __init__(self, files: AsyncFilesResource) -> None: self._files = files + self.retrieve = async_to_raw_response_wrapper( + files.retrieve, + ) self.list = async_to_raw_response_wrapper( files.list, ) + self.delete = async_to_raw_response_wrapper( + files.delete, + ) + self.download = async_to_custom_raw_response_wrapper( + files.download, + AsyncBinaryAPIResponse, + ) self.upload = async_to_raw_response_wrapper( files.upload, ) @@ -285,9 +514,19 @@ class FilesResourceWithStreamingResponse: def __init__(self, files: FilesResource) -> None: self._files = files + self.retrieve = to_streamed_response_wrapper( + files.retrieve, + ) self.list = to_streamed_response_wrapper( files.list, ) + self.delete = to_streamed_response_wrapper( + files.delete, + ) + self.download = to_custom_streamed_response_wrapper( + files.download, + StreamedBinaryAPIResponse, + ) self.upload = to_streamed_response_wrapper( files.upload, ) @@ -297,9 +536,19 @@ class AsyncFilesResourceWithStreamingResponse: def __init__(self, files: AsyncFilesResource) -> None: self._files = files + self.retrieve = async_to_streamed_response_wrapper( + files.retrieve, + ) self.list = async_to_streamed_response_wrapper( files.list, ) + self.delete = async_to_streamed_response_wrapper( + files.delete, + ) + self.download = async_to_custom_streamed_response_wrapper( + files.download, + AsyncStreamedBinaryAPIResponse, + ) self.upload = async_to_streamed_response_wrapper( files.upload, ) diff --git a/src/writerai/types/__init__.py b/src/writerai/types/__init__.py index b2e31c1b..0e549136 100644 --- a/src/writerai/types/__init__.py +++ b/src/writerai/types/__init__.py @@ -15,6 +15,7 @@ from .graph_create_params import GraphCreateParams as GraphCreateParams from .graph_update_params import GraphUpdateParams as GraphUpdateParams from .model_list_response import ModelListResponse as ModelListResponse +from .file_delete_response import FileDeleteResponse as FileDeleteResponse from .graph_create_response import GraphCreateResponse as GraphCreateResponse from .graph_delete_response import GraphDeleteResponse as GraphDeleteResponse from .graph_update_response import GraphUpdateResponse as GraphUpdateResponse diff --git a/src/writerai/types/file_delete_response.py b/src/writerai/types/file_delete_response.py new file mode 100644 index 00000000..3ab627d5 --- /dev/null +++ b/src/writerai/types/file_delete_response.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + + + +from .._models import BaseModel + +__all__ = ["FileDeleteResponse"] + + +class FileDeleteResponse(BaseModel): + id: str + """A unique identifier of the deleted graph.""" + + deleted: bool + """Indicates whether the graph was successfully deleted.""" diff --git a/tests/api_resources/test_files.py b/tests/api_resources/test_files.py index 925cde45..af5d40a1 100644 --- a/tests/api_resources/test_files.py +++ b/tests/api_resources/test_files.py @@ -5,11 +5,19 @@ import os from typing import Any, cast +import httpx import pytest +from respx import MockRouter from writerai import Writer, AsyncWriter from tests.utils import assert_matches_type -from writerai.types import File +from writerai.types import File, FileDeleteResponse +from writerai._response import ( + BinaryAPIResponse, + AsyncBinaryAPIResponse, + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, +) from writerai.pagination import SyncCursorPage, AsyncCursorPage base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -18,6 +26,44 @@ class TestFiles: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + @parametrize + def test_method_retrieve(self, client: Writer) -> None: + file = client.files.retrieve( + "file_id", + ) + assert_matches_type(File, file, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Writer) -> None: + response = client.files.with_raw_response.retrieve( + "file_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + file = response.parse() + assert_matches_type(File, file, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Writer) -> None: + with client.files.with_streaming_response.retrieve( + "file_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + file = response.parse() + assert_matches_type(File, file, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Writer) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): + client.files.with_raw_response.retrieve( + "", + ) + @parametrize def test_method_list(self, client: Writer) -> None: file = client.files.list() @@ -54,6 +100,98 @@ def test_streaming_response_list(self, client: Writer) -> None: assert cast(Any, response.is_closed) is True + @parametrize + def test_method_delete(self, client: Writer) -> None: + file = client.files.delete( + "file_id", + ) + assert_matches_type(FileDeleteResponse, file, path=["response"]) + + @parametrize + def test_raw_response_delete(self, client: Writer) -> None: + response = client.files.with_raw_response.delete( + "file_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + file = response.parse() + assert_matches_type(FileDeleteResponse, file, path=["response"]) + + @parametrize + def test_streaming_response_delete(self, client: Writer) -> None: + with client.files.with_streaming_response.delete( + "file_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + file = response.parse() + assert_matches_type(FileDeleteResponse, file, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_delete(self, client: Writer) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): + client.files.with_raw_response.delete( + "", + ) + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_method_download(self, client: Writer, respx_mock: MockRouter) -> None: + respx_mock.get("/v1/files/file_id/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + file = client.files.download( + "file_id", + ) + assert file.is_closed + assert file.json() == {"foo": "bar"} + assert cast(Any, file.is_closed) is True + assert isinstance(file, BinaryAPIResponse) + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_raw_response_download(self, client: Writer, respx_mock: MockRouter) -> None: + respx_mock.get("/v1/files/file_id/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + file = client.files.with_raw_response.download( + "file_id", + ) + + assert file.is_closed is True + assert file.http_request.headers.get("X-Stainless-Lang") == "python" + assert file.json() == {"foo": "bar"} + assert isinstance(file, BinaryAPIResponse) + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_streaming_response_download(self, client: Writer, respx_mock: MockRouter) -> None: + respx_mock.get("/v1/files/file_id/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + with client.files.with_streaming_response.download( + "file_id", + ) as file: + assert not file.is_closed + assert file.http_request.headers.get("X-Stainless-Lang") == "python" + + assert file.json() == {"foo": "bar"} + assert cast(Any, file.is_closed) is True + assert isinstance(file, StreamedBinaryAPIResponse) + + assert cast(Any, file.is_closed) is True + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_path_params_download(self, client: Writer) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): + client.files.with_raw_response.download( + "", + ) + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") @parametrize def test_method_upload(self, client: Writer) -> None: @@ -101,6 +239,44 @@ def test_streaming_response_upload(self, client: Writer) -> None: class TestAsyncFiles: parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + @parametrize + async def test_method_retrieve(self, async_client: AsyncWriter) -> None: + file = await async_client.files.retrieve( + "file_id", + ) + assert_matches_type(File, file, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncWriter) -> None: + response = await async_client.files.with_raw_response.retrieve( + "file_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + file = await response.parse() + assert_matches_type(File, file, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncWriter) -> None: + async with async_client.files.with_streaming_response.retrieve( + "file_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + file = await response.parse() + assert_matches_type(File, file, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncWriter) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): + await async_client.files.with_raw_response.retrieve( + "", + ) + @parametrize async def test_method_list(self, async_client: AsyncWriter) -> None: file = await async_client.files.list() @@ -137,6 +313,98 @@ async def test_streaming_response_list(self, async_client: AsyncWriter) -> None: assert cast(Any, response.is_closed) is True + @parametrize + async def test_method_delete(self, async_client: AsyncWriter) -> None: + file = await async_client.files.delete( + "file_id", + ) + assert_matches_type(FileDeleteResponse, file, path=["response"]) + + @parametrize + async def test_raw_response_delete(self, async_client: AsyncWriter) -> None: + response = await async_client.files.with_raw_response.delete( + "file_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + file = await response.parse() + assert_matches_type(FileDeleteResponse, file, path=["response"]) + + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncWriter) -> None: + async with async_client.files.with_streaming_response.delete( + "file_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + file = await response.parse() + assert_matches_type(FileDeleteResponse, file, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_delete(self, async_client: AsyncWriter) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): + await async_client.files.with_raw_response.delete( + "", + ) + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_method_download(self, async_client: AsyncWriter, respx_mock: MockRouter) -> None: + respx_mock.get("/v1/files/file_id/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + file = await async_client.files.download( + "file_id", + ) + assert file.is_closed + assert await file.json() == {"foo": "bar"} + assert cast(Any, file.is_closed) is True + assert isinstance(file, AsyncBinaryAPIResponse) + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_raw_response_download(self, async_client: AsyncWriter, respx_mock: MockRouter) -> None: + respx_mock.get("/v1/files/file_id/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + file = await async_client.files.with_raw_response.download( + "file_id", + ) + + assert file.is_closed is True + assert file.http_request.headers.get("X-Stainless-Lang") == "python" + assert await file.json() == {"foo": "bar"} + assert isinstance(file, AsyncBinaryAPIResponse) + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_streaming_response_download(self, async_client: AsyncWriter, respx_mock: MockRouter) -> None: + respx_mock.get("/v1/files/file_id/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + async with async_client.files.with_streaming_response.download( + "file_id", + ) as file: + assert not file.is_closed + assert file.http_request.headers.get("X-Stainless-Lang") == "python" + + assert await file.json() == {"foo": "bar"} + assert cast(Any, file.is_closed) is True + assert isinstance(file, AsyncStreamedBinaryAPIResponse) + + assert cast(Any, file.is_closed) is True + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_path_params_download(self, async_client: AsyncWriter) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): + await async_client.files.with_raw_response.download( + "", + ) + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") @parametrize async def test_method_upload(self, async_client: AsyncWriter) -> None: From f948875d03d7810397d3c1fed1a41577a9c04f41 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 14 Jul 2024 14:07:48 +0000 Subject: [PATCH 043/399] feat(api): OpenAPI spec update via Stainless API (#33) --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 468de8f6..caf456e5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 15 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-2aa0ec0ea99873da5dac6ecf610efdd6ccfe5601d2400f8c475d994cd47c2c36.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-9950942f4d28d7155ca24b353375665668ba7b19f6a381d42651dd4890a0027f.yml From e5651a1e270b013142cc4e7325ea69e91f250bea Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 15 Jul 2024 09:49:03 +0000 Subject: [PATCH 044/399] chore(internal): version bump (#34) --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index da59f99e..2aca35ae 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.4.0" + ".": "0.5.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 31bdbc80..c2ef6d41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "0.4.0" +version = "0.5.0" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index 6af8a6b2..0f5f2c5c 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "0.4.0" # x-release-please-version +__version__ = "0.5.0" # x-release-please-version From c3f0dcfb533fd1af3f956473411acb1a964b15f0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 30 Jul 2024 17:25:19 +0000 Subject: [PATCH 045/399] chore(internal): codegen related update (#37) --- .github/workflows/release-doctor.yml | 2 ++ README.md | 8 +++++++- src/writerai/_base_client.py | 12 ++++++------ src/writerai/_compat.py | 6 +++--- src/writerai/resources/files.py | 4 ---- src/writerai/types/file_upload_params.py | 2 -- tests/api_resources/test_files.py | 6 ------ 7 files changed, 18 insertions(+), 22 deletions(-) diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index 93c5ed1a..43d314d3 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -1,6 +1,8 @@ name: Release Doctor on: pull_request: + branches: + - main workflow_dispatch: jobs: diff --git a/README.md b/README.md index 2d543d65..f09d9164 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ It is generated with [Stainless](https://www.stainlessapi.com/). ## Documentation -The REST API documentation can be found [on dev.writer.com](https://dev.writer.com/api-guides/introduction). The full API of this library can be found in [api.md](api.md). +The REST API documentation can be found on [dev.writer.com](https://dev.writer.com/api-guides/introduction). The full API of this library can be found in [api.md](api.md). ## Installation @@ -367,6 +367,12 @@ client = Writer( ) ``` +You can also customize the client on a per-request basis by using `with_options()`: + +```python +client.with_options(http_client=DefaultHttpxClient(...)) +``` + ### Managing HTTP resources By default the library closes underlying HTTP connections whenever the client is [garbage collected](https://docs.python.org/3/reference/datamodel.html#object.__del__). You can manually close the client using the `.close()` method if desired, or with a context manager that closes when exiting. diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index 920075c8..2315f571 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -879,9 +879,9 @@ def __exit__( def _prepare_options( self, options: FinalRequestOptions, # noqa: ARG002 - ) -> None: + ) -> FinalRequestOptions: """Hook for mutating the given options""" - return None + return options def _prepare_request( self, @@ -961,7 +961,7 @@ def _request( input_options = model_copy(options) cast_to = self._maybe_override_cast_to(cast_to, options) - self._prepare_options(options) + options = self._prepare_options(options) retries = self._remaining_retries(remaining_retries, options) request = self._build_request(options) @@ -1442,9 +1442,9 @@ async def __aexit__( async def _prepare_options( self, options: FinalRequestOptions, # noqa: ARG002 - ) -> None: + ) -> FinalRequestOptions: """Hook for mutating the given options""" - return None + return options async def _prepare_request( self, @@ -1529,7 +1529,7 @@ async def _request( input_options = model_copy(options) cast_to = self._maybe_override_cast_to(cast_to, options) - await self._prepare_options(options) + options = await self._prepare_options(options) retries = self._remaining_retries(remaining_retries, options) request = self._build_request(options) diff --git a/src/writerai/_compat.py b/src/writerai/_compat.py index 74c7639b..c919b5ad 100644 --- a/src/writerai/_compat.py +++ b/src/writerai/_compat.py @@ -118,10 +118,10 @@ def get_model_fields(model: type[pydantic.BaseModel]) -> dict[str, FieldInfo]: return model.__fields__ # type: ignore -def model_copy(model: _ModelT) -> _ModelT: +def model_copy(model: _ModelT, *, deep: bool = False) -> _ModelT: if PYDANTIC_V2: - return model.model_copy() - return model.copy() # type: ignore + return model.model_copy(deep=deep) + return model.copy(deep=deep) # type: ignore def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str: diff --git a/src/writerai/resources/files.py b/src/writerai/resources/files.py index 11df91bc..ee3b20ed 100644 --- a/src/writerai/resources/files.py +++ b/src/writerai/resources/files.py @@ -214,7 +214,6 @@ def upload( *, content: FileTypes, content_disposition: str, - content_length: int, content_type: str, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -237,7 +236,6 @@ def upload( """ extra_headers = { "Content-Disposition": content_disposition, - "Content-Length": str(content_length), "Content-Type": content_type, **(extra_headers or {}), } @@ -429,7 +427,6 @@ async def upload( *, content: FileTypes, content_disposition: str, - content_length: int, content_type: str, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -452,7 +449,6 @@ async def upload( """ extra_headers = { "Content-Disposition": content_disposition, - "Content-Length": str(content_length), "Content-Type": content_type, **(extra_headers or {}), } diff --git a/src/writerai/types/file_upload_params.py b/src/writerai/types/file_upload_params.py index 4a47b726..14077b97 100644 --- a/src/writerai/types/file_upload_params.py +++ b/src/writerai/types/file_upload_params.py @@ -15,6 +15,4 @@ class FileUploadParams(TypedDict, total=False): content_disposition: Required[Annotated[str, PropertyInfo(alias="Content-Disposition")]] - content_length: Required[Annotated[int, PropertyInfo(alias="Content-Length")]] - content_type: Required[Annotated[str, PropertyInfo(alias="Content-Type")]] diff --git a/tests/api_resources/test_files.py b/tests/api_resources/test_files.py index af5d40a1..6e022684 100644 --- a/tests/api_resources/test_files.py +++ b/tests/api_resources/test_files.py @@ -198,7 +198,6 @@ def test_method_upload(self, client: Writer) -> None: file = client.files.upload( content=b"raw file contents", content_disposition="Content-Disposition", - content_length=0, content_type="Content-Type", ) assert_matches_type(File, file, path=["response"]) @@ -209,7 +208,6 @@ def test_raw_response_upload(self, client: Writer) -> None: response = client.files.with_raw_response.upload( content=b"raw file contents", content_disposition="Content-Disposition", - content_length=0, content_type="Content-Type", ) @@ -224,7 +222,6 @@ def test_streaming_response_upload(self, client: Writer) -> None: with client.files.with_streaming_response.upload( content=b"raw file contents", content_disposition="Content-Disposition", - content_length=0, content_type="Content-Type", ) as response: assert not response.is_closed @@ -411,7 +408,6 @@ async def test_method_upload(self, async_client: AsyncWriter) -> None: file = await async_client.files.upload( content=b"raw file contents", content_disposition="Content-Disposition", - content_length=0, content_type="Content-Type", ) assert_matches_type(File, file, path=["response"]) @@ -422,7 +418,6 @@ async def test_raw_response_upload(self, async_client: AsyncWriter) -> None: response = await async_client.files.with_raw_response.upload( content=b"raw file contents", content_disposition="Content-Disposition", - content_length=0, content_type="Content-Type", ) @@ -437,7 +432,6 @@ async def test_streaming_response_upload(self, async_client: AsyncWriter) -> Non async with async_client.files.with_streaming_response.upload( content=b"raw file contents", content_disposition="Content-Disposition", - content_length=0, content_type="Content-Type", ) as response: assert not response.is_closed From ba452c114c4a470b99aad1f96b70812ac613641b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 30 Jul 2024 17:27:10 +0000 Subject: [PATCH 046/399] chore(tests): update prism version (#38) --- scripts/mock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/mock b/scripts/mock index fe89a1d0..f5861576 100755 --- a/scripts/mock +++ b/scripts/mock @@ -21,7 +21,7 @@ echo "==> Starting mock server with URL ${URL}" # Run prism mock on the given spec if [ "$1" == "--daemon" ]; then - npm exec --package=@stoplight/prism-cli@~5.8 -- prism mock "$URL" &> .prism.log & + npm exec --package=@stainless-api/prism-cli@5.8.4 -- prism mock "$URL" &> .prism.log & # Wait for server to come online echo -n "Waiting for server" @@ -37,5 +37,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stoplight/prism-cli@~5.8 -- prism mock "$URL" + npm exec --package=@stainless-api/prism-cli@5.8.4 -- prism mock "$URL" fi From 0abf1aadd1b816ea6bf5a8d5068d3d692860f753 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 30 Jul 2024 17:29:22 +0000 Subject: [PATCH 047/399] chore: fix error message import example (#39) --- src/writerai/_base_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index 2315f571..11f7e743 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -363,7 +363,7 @@ def __init__( if max_retries is None: # pyright: ignore[reportUnnecessaryComparison] raise TypeError( - "max_retries cannot be None. If you want to disable retries, pass `0`; if you want unlimited retries, pass `math.inf` or a very high number; if you want the default behavior, pass `writer-sdk.DEFAULT_MAX_RETRIES`" + "max_retries cannot be None. If you want to disable retries, pass `0`; if you want unlimited retries, pass `math.inf` or a very high number; if you want the default behavior, pass `writerai.DEFAULT_MAX_RETRIES`" ) def _enforce_trailing_slash(self, url: URL) -> URL: From 61a29407993330720b6e83bd88d5fd80f2f3ef1e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 30 Jul 2024 17:32:09 +0000 Subject: [PATCH 048/399] chore(internal): add type construction helper (#40) --- src/writerai/_models.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/writerai/_models.py b/src/writerai/_models.py index eb7ce3bd..5148d5a7 100644 --- a/src/writerai/_models.py +++ b/src/writerai/_models.py @@ -406,6 +406,15 @@ def build( return cast(_BaseModelT, construct_type(type_=base_model_cls, value=kwargs)) +def construct_type_unchecked(*, value: object, type_: type[_T]) -> _T: + """Loose coercion to the expected type with construction of nested values. + + Note: the returned value from this function is not guaranteed to match the + given type. + """ + return cast(_T, construct_type(value=value, type_=type_)) + + def construct_type(*, value: object, type_: object) -> object: """Loose coercion to the expected type with construction of nested values. From 27f2da93a630b5b061143e906840731ebfc0d50a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 1 Aug 2024 11:37:32 +0000 Subject: [PATCH 049/399] feat(api): update via SDK Studio (#41) --- README.md | 18 +++++++++--------- tests/test_client.py | 8 ++++---- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index f09d9164..cc47e253 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ chat = client.chat.chat( "role": "user", } ], - model="palmyra-x-32k", + model="palmyra-x-002-32k", ) print(chat.id) ``` @@ -72,7 +72,7 @@ async def main() -> None: "role": "user", } ], - model="palmyra-x-32k", + model="palmyra-x-002-32k", ) print(chat.id) @@ -92,7 +92,7 @@ from writerai import Writer client = Writer() stream = client.completions.create( - model="palmyra-x-32k", + model="palmyra-x-002-32k", prompt="Hi, my name is", stream=True, ) @@ -108,7 +108,7 @@ from writerai import AsyncWriter client = AsyncWriter() stream = await client.completions.create( - model="palmyra-x-32k", + model="palmyra-x-002-32k", prompt="Hi, my name is", stream=True, ) @@ -148,7 +148,7 @@ try: "role": "user", } ], - model="palmyra-x-32k", + model="palmyra-x-002-32k", ) except writerai.APIConnectionError as e: print("The server could not be reached") @@ -199,7 +199,7 @@ client.with_options(max_retries=5).chat.chat( "role": "user", } ], - model="palmyra-x-32k", + model="palmyra-x-002-32k", ) ``` @@ -230,7 +230,7 @@ client.with_options(timeout=5.0).chat.chat( "role": "user", } ], - model="palmyra-x-32k", + model="palmyra-x-002-32k", ) ``` @@ -275,7 +275,7 @@ response = client.chat.with_raw_response.chat( "content": "content", "role": "user", }], - model="palmyra-x-32k", + model="palmyra-x-002-32k", ) print(response.headers.get('X-My-Header')) @@ -301,7 +301,7 @@ with client.chat.with_streaming_response.chat( "role": "user", } ], - model="palmyra-x-32k", + model="palmyra-x-002-32k", ) as response: print(response.headers.get("X-My-Header")) diff --git a/tests/test_client.py b/tests/test_client.py index 10b0faac..c6098095 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -728,7 +728,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> No "role": "user", } ], - model="palmyra-x-32k", + model="palmyra-x-002-32k", ), ), cast_to=httpx.Response, @@ -754,7 +754,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> Non "role": "user", } ], - model="palmyra-x-32k", + model="palmyra-x-002-32k", ), ), cast_to=httpx.Response, @@ -1459,7 +1459,7 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) "role": "user", } ], - model="palmyra-x-32k", + model="palmyra-x-002-32k", ), ), cast_to=httpx.Response, @@ -1485,7 +1485,7 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) "role": "user", } ], - model="palmyra-x-32k", + model="palmyra-x-002-32k", ), ), cast_to=httpx.Response, From a392f1bbf7cb17d9b0ea8699e0a14b36db382e82 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 1 Aug 2024 11:37:51 +0000 Subject: [PATCH 050/399] chore(internal): codegen related update (#43) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cc47e253..f09d90b5 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ from writerai import Writer client = Writer() stream = client.completions.create( - model="palmyra-x-002-32k", + model="palmyra-x-002-instruct", prompt="Hi, my name is", stream=True, ) @@ -108,7 +108,7 @@ from writerai import AsyncWriter client = AsyncWriter() stream = await client.completions.create( - model="palmyra-x-002-32k", + model="palmyra-x-002-instruct", prompt="Hi, my name is", stream=True, ) From 51d1a550926afa208547b619462cd1ef9087b560 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 1 Aug 2024 11:38:19 +0000 Subject: [PATCH 051/399] chore(internal): update example values (#44) --- tests/api_resources/test_completions.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/api_resources/test_completions.py b/tests/api_resources/test_completions.py index 4db055fa..428bab7d 100644 --- a/tests/api_resources/test_completions.py +++ b/tests/api_resources/test_completions.py @@ -71,7 +71,7 @@ def test_method_create_overload_2(self, client: Writer) -> None: completion_stream = client.completions.create( model="palmyra-x-002-instruct", prompt="Write me an SEO article about...", - stream=True, + stream=False, ) completion_stream.response.close() @@ -80,7 +80,7 @@ def test_method_create_with_all_params_overload_2(self, client: Writer) -> None: completion_stream = client.completions.create( model="palmyra-x-002-instruct", prompt="Write me an SEO article about...", - stream=True, + stream=False, best_of=1, max_tokens=150, random_seed=42, @@ -95,7 +95,7 @@ def test_raw_response_create_overload_2(self, client: Writer) -> None: response = client.completions.with_raw_response.create( model="palmyra-x-002-instruct", prompt="Write me an SEO article about...", - stream=True, + stream=False, ) assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -107,7 +107,7 @@ def test_streaming_response_create_overload_2(self, client: Writer) -> None: with client.completions.with_streaming_response.create( model="palmyra-x-002-instruct", prompt="Write me an SEO article about...", - stream=True, + stream=False, ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -175,7 +175,7 @@ async def test_method_create_overload_2(self, async_client: AsyncWriter) -> None completion_stream = await async_client.completions.create( model="palmyra-x-002-instruct", prompt="Write me an SEO article about...", - stream=True, + stream=False, ) await completion_stream.response.aclose() @@ -184,7 +184,7 @@ async def test_method_create_with_all_params_overload_2(self, async_client: Asyn completion_stream = await async_client.completions.create( model="palmyra-x-002-instruct", prompt="Write me an SEO article about...", - stream=True, + stream=False, best_of=1, max_tokens=150, random_seed=42, @@ -199,7 +199,7 @@ async def test_raw_response_create_overload_2(self, async_client: AsyncWriter) - response = await async_client.completions.with_raw_response.create( model="palmyra-x-002-instruct", prompt="Write me an SEO article about...", - stream=True, + stream=False, ) assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -211,7 +211,7 @@ async def test_streaming_response_create_overload_2(self, async_client: AsyncWri async with async_client.completions.with_streaming_response.create( model="palmyra-x-002-instruct", prompt="Write me an SEO article about...", - stream=True, + stream=False, ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" From 30eaafb86d875f4f22bfa74384172fb3a51dca4d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 1 Aug 2024 11:49:01 +0000 Subject: [PATCH 052/399] feat(api): update via SDK Studio (#45) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f09d90b5..96c81ba5 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ client = Writer() stream = client.completions.create( model="palmyra-x-002-instruct", - prompt="Hi, my name is", + prompt="Hi, my name is .", stream=True, ) for completion in stream: @@ -109,7 +109,7 @@ client = AsyncWriter() stream = await client.completions.create( model="palmyra-x-002-instruct", - prompt="Hi, my name is", + prompt="Hi, my name is .", stream=True, ) async for completion in stream: From a3b64dec996c29ac080024054f7761c6af3165fb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 1 Aug 2024 11:49:24 +0000 Subject: [PATCH 053/399] feat(api): update via SDK Studio (#46) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 96c81ba5..f09d90b5 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ client = Writer() stream = client.completions.create( model="palmyra-x-002-instruct", - prompt="Hi, my name is .", + prompt="Hi, my name is", stream=True, ) for completion in stream: @@ -109,7 +109,7 @@ client = AsyncWriter() stream = await client.completions.create( model="palmyra-x-002-instruct", - prompt="Hi, my name is .", + prompt="Hi, my name is", stream=True, ) async for completion in stream: From a61e5178de4f94b2037f65c23561953815af0f7f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 8 Aug 2024 13:16:41 +0000 Subject: [PATCH 054/399] feat(api): added method to generate applications content (#47) --- .stats.yml | 4 +- api.md | 12 + src/writerai/_client.py | 8 + src/writerai/resources/__init__.py | 14 ++ src/writerai/resources/applications.py | 156 +++++++++++++ src/writerai/resources/files.py | 4 +- src/writerai/resources/graphs.py | 52 +++-- src/writerai/types/__init__.py | 4 + .../application_generate_content_params.py | 18 ++ .../application_generate_content_response.py | 13 ++ src/writerai/types/file.py | 6 +- src/writerai/types/file_delete_response.py | 4 +- src/writerai/types/graph.py | 6 +- src/writerai/types/graph_create_params.py | 10 +- src/writerai/types/graph_create_response.py | 4 +- src/writerai/types/graph_delete_response.py | 4 +- .../graph_remove_file_from_graph_response.py | 4 +- src/writerai/types/graph_update_params.py | 10 +- src/writerai/types/graph_update_response.py | 4 +- tests/api_resources/test_applications.py | 210 ++++++++++++++++++ tests/api_resources/test_graphs.py | 40 +--- 21 files changed, 503 insertions(+), 84 deletions(-) create mode 100644 src/writerai/resources/applications.py create mode 100644 src/writerai/types/application_generate_content_params.py create mode 100644 src/writerai/types/application_generate_content_response.py create mode 100644 tests/api_resources/test_applications.py diff --git a/.stats.yml b/.stats.yml index caf456e5..789612fa 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ -configured_endpoints: 15 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-9950942f4d28d7155ca24b353375665668ba7b19f6a381d42651dd4890a0027f.yml +configured_endpoints: 16 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-4a89aa730a0c5dad44fa86cfa4f14b10fa44a076dfe6d5d6a2eb1cf8569bf526.yml diff --git a/api.md b/api.md index abecbd8a..0d13ba2a 100644 --- a/api.md +++ b/api.md @@ -1,3 +1,15 @@ +# Applications + +Types: + +```python +from writerai.types import ApplicationGenerateContentResponse +``` + +Methods: + +- client.applications.generate_content(application_id, \*\*params) -> ApplicationGenerateContentResponse + # Chat Types: diff --git a/src/writerai/_client.py b/src/writerai/_client.py index ab243071..120b9340 100644 --- a/src/writerai/_client.py +++ b/src/writerai/_client.py @@ -46,6 +46,7 @@ class Writer(SyncAPIClient): + applications: resources.ApplicationsResource chat: resources.ChatResource completions: resources.CompletionsResource models: resources.ModelsResource @@ -110,6 +111,7 @@ def __init__( self._default_stream_cls = Stream + self.applications = resources.ApplicationsResource(self) self.chat = resources.ChatResource(self) self.completions = resources.CompletionsResource(self) self.models = resources.ModelsResource(self) @@ -224,6 +226,7 @@ def _make_status_error( class AsyncWriter(AsyncAPIClient): + applications: resources.AsyncApplicationsResource chat: resources.AsyncChatResource completions: resources.AsyncCompletionsResource models: resources.AsyncModelsResource @@ -288,6 +291,7 @@ def __init__( self._default_stream_cls = AsyncStream + self.applications = resources.AsyncApplicationsResource(self) self.chat = resources.AsyncChatResource(self) self.completions = resources.AsyncCompletionsResource(self) self.models = resources.AsyncModelsResource(self) @@ -403,6 +407,7 @@ def _make_status_error( class WriterWithRawResponse: def __init__(self, client: Writer) -> None: + self.applications = resources.ApplicationsResourceWithRawResponse(client.applications) self.chat = resources.ChatResourceWithRawResponse(client.chat) self.completions = resources.CompletionsResourceWithRawResponse(client.completions) self.models = resources.ModelsResourceWithRawResponse(client.models) @@ -412,6 +417,7 @@ def __init__(self, client: Writer) -> None: class AsyncWriterWithRawResponse: def __init__(self, client: AsyncWriter) -> None: + self.applications = resources.AsyncApplicationsResourceWithRawResponse(client.applications) self.chat = resources.AsyncChatResourceWithRawResponse(client.chat) self.completions = resources.AsyncCompletionsResourceWithRawResponse(client.completions) self.models = resources.AsyncModelsResourceWithRawResponse(client.models) @@ -421,6 +427,7 @@ def __init__(self, client: AsyncWriter) -> None: class WriterWithStreamedResponse: def __init__(self, client: Writer) -> None: + self.applications = resources.ApplicationsResourceWithStreamingResponse(client.applications) self.chat = resources.ChatResourceWithStreamingResponse(client.chat) self.completions = resources.CompletionsResourceWithStreamingResponse(client.completions) self.models = resources.ModelsResourceWithStreamingResponse(client.models) @@ -430,6 +437,7 @@ def __init__(self, client: Writer) -> None: class AsyncWriterWithStreamedResponse: def __init__(self, client: AsyncWriter) -> None: + self.applications = resources.AsyncApplicationsResourceWithStreamingResponse(client.applications) self.chat = resources.AsyncChatResourceWithStreamingResponse(client.chat) self.completions = resources.AsyncCompletionsResourceWithStreamingResponse(client.completions) self.models = resources.AsyncModelsResourceWithStreamingResponse(client.models) diff --git a/src/writerai/resources/__init__.py b/src/writerai/resources/__init__.py index 4d17d607..739eb03b 100644 --- a/src/writerai/resources/__init__.py +++ b/src/writerai/resources/__init__.py @@ -40,8 +40,22 @@ CompletionsResourceWithStreamingResponse, AsyncCompletionsResourceWithStreamingResponse, ) +from .applications import ( + ApplicationsResource, + AsyncApplicationsResource, + ApplicationsResourceWithRawResponse, + AsyncApplicationsResourceWithRawResponse, + ApplicationsResourceWithStreamingResponse, + AsyncApplicationsResourceWithStreamingResponse, +) __all__ = [ + "ApplicationsResource", + "AsyncApplicationsResource", + "ApplicationsResourceWithRawResponse", + "AsyncApplicationsResourceWithRawResponse", + "ApplicationsResourceWithStreamingResponse", + "AsyncApplicationsResourceWithStreamingResponse", "ChatResource", "AsyncChatResource", "ChatResourceWithRawResponse", diff --git a/src/writerai/resources/applications.py b/src/writerai/resources/applications.py new file mode 100644 index 00000000..e32d7404 --- /dev/null +++ b/src/writerai/resources/applications.py @@ -0,0 +1,156 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Iterable + +import httpx + +from ..types import application_generate_content_params +from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from .._utils import ( + maybe_transform, + async_maybe_transform, +) +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.application_generate_content_response import ApplicationGenerateContentResponse + +__all__ = ["ApplicationsResource", "AsyncApplicationsResource"] + + +class ApplicationsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> ApplicationsResourceWithRawResponse: + return ApplicationsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ApplicationsResourceWithStreamingResponse: + return ApplicationsResourceWithStreamingResponse(self) + + def generate_content( + self, + application_id: str, + *, + inputs: Iterable[application_generate_content_params.Input], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ApplicationGenerateContentResponse: + """ + Generate content from an existing application with inputs. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not application_id: + raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") + return self._post( + f"/v1/applications/{application_id}", + body=maybe_transform( + {"inputs": inputs}, application_generate_content_params.ApplicationGenerateContentParams + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ApplicationGenerateContentResponse, + ) + + +class AsyncApplicationsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncApplicationsResourceWithRawResponse: + return AsyncApplicationsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncApplicationsResourceWithStreamingResponse: + return AsyncApplicationsResourceWithStreamingResponse(self) + + async def generate_content( + self, + application_id: str, + *, + inputs: Iterable[application_generate_content_params.Input], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ApplicationGenerateContentResponse: + """ + Generate content from an existing application with inputs. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not application_id: + raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") + return await self._post( + f"/v1/applications/{application_id}", + body=await async_maybe_transform( + {"inputs": inputs}, application_generate_content_params.ApplicationGenerateContentParams + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ApplicationGenerateContentResponse, + ) + + +class ApplicationsResourceWithRawResponse: + def __init__(self, applications: ApplicationsResource) -> None: + self._applications = applications + + self.generate_content = to_raw_response_wrapper( + applications.generate_content, + ) + + +class AsyncApplicationsResourceWithRawResponse: + def __init__(self, applications: AsyncApplicationsResource) -> None: + self._applications = applications + + self.generate_content = async_to_raw_response_wrapper( + applications.generate_content, + ) + + +class ApplicationsResourceWithStreamingResponse: + def __init__(self, applications: ApplicationsResource) -> None: + self._applications = applications + + self.generate_content = to_streamed_response_wrapper( + applications.generate_content, + ) + + +class AsyncApplicationsResourceWithStreamingResponse: + def __init__(self, applications: AsyncApplicationsResource) -> None: + self._applications = applications + + self.generate_content = async_to_streamed_response_wrapper( + applications.generate_content, + ) diff --git a/src/writerai/resources/files.py b/src/writerai/resources/files.py index ee3b20ed..5ce78ad2 100644 --- a/src/writerai/resources/files.py +++ b/src/writerai/resources/files.py @@ -200,7 +200,7 @@ def download( """ if not file_id: raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") - extra_headers = {"Accept": "file contents", **(extra_headers or {})} + extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} return self._get( f"/v1/files/{file_id}/download", options=make_request_options( @@ -413,7 +413,7 @@ async def download( """ if not file_id: raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") - extra_headers = {"Accept": "file contents", **(extra_headers or {})} + extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} return await self._get( f"/v1/files/{file_id}/download", options=make_request_options( diff --git a/src/writerai/resources/graphs.py b/src/writerai/resources/graphs.py index b31dac45..e157ad8d 100644 --- a/src/writerai/resources/graphs.py +++ b/src/writerai/resources/graphs.py @@ -49,8 +49,8 @@ def with_streaming_response(self) -> GraphsResourceWithStreamingResponse: def create( self, *, - name: str, description: str | NotGiven = NOT_GIVEN, + name: str | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -58,14 +58,15 @@ def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> GraphCreateResponse: - """ - Create graph + """Create graph Args: - name: The name of the graph. - description: A description of the graph. + This can be at most 255 characters. + + name: The name of the graph. This can be at most 255 characters. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -78,8 +79,8 @@ def create( "/v1/graphs", body=maybe_transform( { - "name": name, "description": description, + "name": name, }, graph_create_params.GraphCreateParams, ), @@ -126,8 +127,8 @@ def update( self, graph_id: str, *, - name: str, description: str | NotGiven = NOT_GIVEN, + name: str | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -135,14 +136,15 @@ def update( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> GraphUpdateResponse: - """ - Update graph + """Update graph Args: - name: The name of the graph. - description: A description of the graph. + This can be at most 255 characters. + + name: The name of the graph. This can be at most 255 characters. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -157,8 +159,8 @@ def update( f"/v1/graphs/{graph_id}", body=maybe_transform( { - "name": name, "description": description, + "name": name, }, graph_update_params.GraphUpdateParams, ), @@ -347,8 +349,8 @@ def with_streaming_response(self) -> AsyncGraphsResourceWithStreamingResponse: async def create( self, *, - name: str, description: str | NotGiven = NOT_GIVEN, + name: str | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -356,14 +358,15 @@ async def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> GraphCreateResponse: - """ - Create graph + """Create graph Args: - name: The name of the graph. - description: A description of the graph. + This can be at most 255 characters. + + name: The name of the graph. This can be at most 255 characters. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -376,8 +379,8 @@ async def create( "/v1/graphs", body=await async_maybe_transform( { - "name": name, "description": description, + "name": name, }, graph_create_params.GraphCreateParams, ), @@ -424,8 +427,8 @@ async def update( self, graph_id: str, *, - name: str, description: str | NotGiven = NOT_GIVEN, + name: str | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -433,14 +436,15 @@ async def update( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> GraphUpdateResponse: - """ - Update graph + """Update graph Args: - name: The name of the graph. - description: A description of the graph. + This can be at most 255 characters. + + name: The name of the graph. This can be at most 255 characters. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -455,8 +459,8 @@ async def update( f"/v1/graphs/{graph_id}", body=await async_maybe_transform( { - "name": name, "description": description, + "name": name, }, graph_update_params.GraphUpdateParams, ), diff --git a/src/writerai/types/__init__.py b/src/writerai/types/__init__.py index 0e549136..d76104cb 100644 --- a/src/writerai/types/__init__.py +++ b/src/writerai/types/__init__.py @@ -21,4 +21,8 @@ from .graph_update_response import GraphUpdateResponse as GraphUpdateResponse from .completion_create_params import CompletionCreateParams as CompletionCreateParams from .graph_add_file_to_graph_params import GraphAddFileToGraphParams as GraphAddFileToGraphParams +from .application_generate_content_params import ApplicationGenerateContentParams as ApplicationGenerateContentParams +from .application_generate_content_response import ( + ApplicationGenerateContentResponse as ApplicationGenerateContentResponse, +) from .graph_remove_file_from_graph_response import GraphRemoveFileFromGraphResponse as GraphRemoveFileFromGraphResponse diff --git a/src/writerai/types/application_generate_content_params.py b/src/writerai/types/application_generate_content_params.py new file mode 100644 index 00000000..5ef75a50 --- /dev/null +++ b/src/writerai/types/application_generate_content_params.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Iterable +from typing_extensions import Required, TypedDict + +__all__ = ["ApplicationGenerateContentParams", "Input"] + + +class ApplicationGenerateContentParams(TypedDict, total=False): + inputs: Required[Iterable[Input]] + + +class Input(TypedDict, total=False): + id: Required[str] + + value: Required[List[str]] diff --git a/src/writerai/types/application_generate_content_response.py b/src/writerai/types/application_generate_content_response.py new file mode 100644 index 00000000..5f9e8572 --- /dev/null +++ b/src/writerai/types/application_generate_content_response.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel + +__all__ = ["ApplicationGenerateContentResponse"] + + +class ApplicationGenerateContentResponse(BaseModel): + suggestion: str + + title: Optional[str] = None diff --git a/src/writerai/types/file.py b/src/writerai/types/file.py index 2f8d5630..8b23f13b 100644 --- a/src/writerai/types/file.py +++ b/src/writerai/types/file.py @@ -10,13 +10,13 @@ class File(BaseModel): id: str - """A unique identifier of the graph.""" + """A unique identifier of the file.""" created_at: datetime - """The timestamp when the graph was created.""" + """The timestamp when the file was uploaded.""" graph_ids: List[str] """A list of graph IDs that the file is associated with.""" name: str - """The name of the graph.""" + """The name of the file.""" diff --git a/src/writerai/types/file_delete_response.py b/src/writerai/types/file_delete_response.py index 3ab627d5..7c359f8e 100644 --- a/src/writerai/types/file_delete_response.py +++ b/src/writerai/types/file_delete_response.py @@ -9,7 +9,7 @@ class FileDeleteResponse(BaseModel): id: str - """A unique identifier of the deleted graph.""" + """A unique identifier of the deleted file.""" deleted: bool - """Indicates whether the graph was successfully deleted.""" + """Indicates whether the file was successfully deleted.""" diff --git a/src/writerai/types/graph.py b/src/writerai/types/graph.py index 27816f02..2d83ddc3 100644 --- a/src/writerai/types/graph.py +++ b/src/writerai/types/graph.py @@ -24,15 +24,15 @@ class FileStatus(BaseModel): class Graph(BaseModel): id: str - """A unique identifier of the file.""" + """A unique identifier of the graph.""" created_at: datetime - """The timestamp when the file was created.""" + """The timestamp when the graph was created.""" file_status: FileStatus name: str - """The name of the file.""" + """The name of the graph.""" description: Optional[str] = None """A description of the graph.""" diff --git a/src/writerai/types/graph_create_params.py b/src/writerai/types/graph_create_params.py index 8f1dd274..0c6406ac 100644 --- a/src/writerai/types/graph_create_params.py +++ b/src/writerai/types/graph_create_params.py @@ -2,14 +2,14 @@ from __future__ import annotations -from typing_extensions import Required, TypedDict +from typing_extensions import TypedDict __all__ = ["GraphCreateParams"] class GraphCreateParams(TypedDict, total=False): - name: Required[str] - """The name of the graph.""" - description: str - """A description of the graph.""" + """A description of the graph. This can be at most 255 characters.""" + + name: str + """The name of the graph. This can be at most 255 characters.""" diff --git a/src/writerai/types/graph_create_response.py b/src/writerai/types/graph_create_response.py index 408e77c1..87890b88 100644 --- a/src/writerai/types/graph_create_response.py +++ b/src/writerai/types/graph_create_response.py @@ -16,7 +16,7 @@ class GraphCreateResponse(BaseModel): """The timestamp when the graph was created.""" name: str - """The name of the graph.""" + """The name of the graph. This can be at most 255 characters.""" description: Optional[str] = None - """A description of the graph.""" + """A description of the graph. This can be at most 255 characters.""" diff --git a/src/writerai/types/graph_delete_response.py b/src/writerai/types/graph_delete_response.py index f5aff6b3..dd849fa3 100644 --- a/src/writerai/types/graph_delete_response.py +++ b/src/writerai/types/graph_delete_response.py @@ -9,7 +9,7 @@ class GraphDeleteResponse(BaseModel): id: str - """A unique identifier of the deleted file.""" + """A unique identifier of the deleted graph.""" deleted: bool - """Indicates whether the file was successfully deleted.""" + """Indicates whether the graph was successfully deleted.""" diff --git a/src/writerai/types/graph_remove_file_from_graph_response.py b/src/writerai/types/graph_remove_file_from_graph_response.py index 3adc8c23..7fa56f72 100644 --- a/src/writerai/types/graph_remove_file_from_graph_response.py +++ b/src/writerai/types/graph_remove_file_from_graph_response.py @@ -9,7 +9,7 @@ class GraphRemoveFileFromGraphResponse(BaseModel): id: str - """A unique identifier of the deleted graph.""" + """A unique identifier of the deleted file.""" deleted: bool - """Indicates whether the graph was successfully deleted.""" + """Indicates whether the file was successfully deleted.""" diff --git a/src/writerai/types/graph_update_params.py b/src/writerai/types/graph_update_params.py index b0d734a4..cebca2c7 100644 --- a/src/writerai/types/graph_update_params.py +++ b/src/writerai/types/graph_update_params.py @@ -2,14 +2,14 @@ from __future__ import annotations -from typing_extensions import Required, TypedDict +from typing_extensions import TypedDict __all__ = ["GraphUpdateParams"] class GraphUpdateParams(TypedDict, total=False): - name: Required[str] - """The name of the graph.""" - description: str - """A description of the graph.""" + """A description of the graph. This can be at most 255 characters.""" + + name: str + """The name of the graph. This can be at most 255 characters.""" diff --git a/src/writerai/types/graph_update_response.py b/src/writerai/types/graph_update_response.py index fcc7825f..de4ccbcd 100644 --- a/src/writerai/types/graph_update_response.py +++ b/src/writerai/types/graph_update_response.py @@ -16,7 +16,7 @@ class GraphUpdateResponse(BaseModel): """The timestamp when the graph was created.""" name: str - """The name of the graph.""" + """The name of the graph. This can be at most 255 characters.""" description: Optional[str] = None - """A description of the graph.""" + """A description of the graph. This can be at most 255 characters.""" diff --git a/tests/api_resources/test_applications.py b/tests/api_resources/test_applications.py new file mode 100644 index 00000000..f1063ba3 --- /dev/null +++ b/tests/api_resources/test_applications.py @@ -0,0 +1,210 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from writerai import Writer, AsyncWriter +from tests.utils import assert_matches_type +from writerai.types import ApplicationGenerateContentResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestApplications: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_generate_content(self, client: Writer) -> None: + application = client.applications.generate_content( + application_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + inputs=[ + { + "id": "id", + "value": ["string", "string", "string"], + }, + { + "id": "id", + "value": ["string", "string", "string"], + }, + { + "id": "id", + "value": ["string", "string", "string"], + }, + ], + ) + assert_matches_type(ApplicationGenerateContentResponse, application, path=["response"]) + + @parametrize + def test_raw_response_generate_content(self, client: Writer) -> None: + response = client.applications.with_raw_response.generate_content( + application_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + inputs=[ + { + "id": "id", + "value": ["string", "string", "string"], + }, + { + "id": "id", + "value": ["string", "string", "string"], + }, + { + "id": "id", + "value": ["string", "string", "string"], + }, + ], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + application = response.parse() + assert_matches_type(ApplicationGenerateContentResponse, application, path=["response"]) + + @parametrize + def test_streaming_response_generate_content(self, client: Writer) -> None: + with client.applications.with_streaming_response.generate_content( + application_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + inputs=[ + { + "id": "id", + "value": ["string", "string", "string"], + }, + { + "id": "id", + "value": ["string", "string", "string"], + }, + { + "id": "id", + "value": ["string", "string", "string"], + }, + ], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + application = response.parse() + assert_matches_type(ApplicationGenerateContentResponse, application, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_generate_content(self, client: Writer) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `application_id` but received ''"): + client.applications.with_raw_response.generate_content( + application_id="", + inputs=[ + { + "id": "id", + "value": ["string", "string", "string"], + }, + { + "id": "id", + "value": ["string", "string", "string"], + }, + { + "id": "id", + "value": ["string", "string", "string"], + }, + ], + ) + + +class TestAsyncApplications: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_generate_content(self, async_client: AsyncWriter) -> None: + application = await async_client.applications.generate_content( + application_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + inputs=[ + { + "id": "id", + "value": ["string", "string", "string"], + }, + { + "id": "id", + "value": ["string", "string", "string"], + }, + { + "id": "id", + "value": ["string", "string", "string"], + }, + ], + ) + assert_matches_type(ApplicationGenerateContentResponse, application, path=["response"]) + + @parametrize + async def test_raw_response_generate_content(self, async_client: AsyncWriter) -> None: + response = await async_client.applications.with_raw_response.generate_content( + application_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + inputs=[ + { + "id": "id", + "value": ["string", "string", "string"], + }, + { + "id": "id", + "value": ["string", "string", "string"], + }, + { + "id": "id", + "value": ["string", "string", "string"], + }, + ], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + application = await response.parse() + assert_matches_type(ApplicationGenerateContentResponse, application, path=["response"]) + + @parametrize + async def test_streaming_response_generate_content(self, async_client: AsyncWriter) -> None: + async with async_client.applications.with_streaming_response.generate_content( + application_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + inputs=[ + { + "id": "id", + "value": ["string", "string", "string"], + }, + { + "id": "id", + "value": ["string", "string", "string"], + }, + { + "id": "id", + "value": ["string", "string", "string"], + }, + ], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + application = await response.parse() + assert_matches_type(ApplicationGenerateContentResponse, application, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_generate_content(self, async_client: AsyncWriter) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `application_id` but received ''"): + await async_client.applications.with_raw_response.generate_content( + application_id="", + inputs=[ + { + "id": "id", + "value": ["string", "string", "string"], + }, + { + "id": "id", + "value": ["string", "string", "string"], + }, + { + "id": "id", + "value": ["string", "string", "string"], + }, + ], + ) diff --git a/tests/api_resources/test_graphs.py b/tests/api_resources/test_graphs.py index a25da3a4..0cd2383e 100644 --- a/tests/api_resources/test_graphs.py +++ b/tests/api_resources/test_graphs.py @@ -27,24 +27,20 @@ class TestGraphs: @parametrize def test_method_create(self, client: Writer) -> None: - graph = client.graphs.create( - name="name", - ) + graph = client.graphs.create() assert_matches_type(GraphCreateResponse, graph, path=["response"]) @parametrize def test_method_create_with_all_params(self, client: Writer) -> None: graph = client.graphs.create( - name="name", description="description", + name="name", ) assert_matches_type(GraphCreateResponse, graph, path=["response"]) @parametrize def test_raw_response_create(self, client: Writer) -> None: - response = client.graphs.with_raw_response.create( - name="name", - ) + response = client.graphs.with_raw_response.create() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -53,9 +49,7 @@ def test_raw_response_create(self, client: Writer) -> None: @parametrize def test_streaming_response_create(self, client: Writer) -> None: - with client.graphs.with_streaming_response.create( - name="name", - ) as response: + with client.graphs.with_streaming_response.create() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -106,7 +100,6 @@ def test_path_params_retrieve(self, client: Writer) -> None: def test_method_update(self, client: Writer) -> None: graph = client.graphs.update( graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - name="name", ) assert_matches_type(GraphUpdateResponse, graph, path=["response"]) @@ -114,8 +107,8 @@ def test_method_update(self, client: Writer) -> None: def test_method_update_with_all_params(self, client: Writer) -> None: graph = client.graphs.update( graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - name="name", description="description", + name="name", ) assert_matches_type(GraphUpdateResponse, graph, path=["response"]) @@ -123,7 +116,6 @@ def test_method_update_with_all_params(self, client: Writer) -> None: def test_raw_response_update(self, client: Writer) -> None: response = client.graphs.with_raw_response.update( graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - name="name", ) assert response.is_closed is True @@ -135,7 +127,6 @@ def test_raw_response_update(self, client: Writer) -> None: def test_streaming_response_update(self, client: Writer) -> None: with client.graphs.with_streaming_response.update( graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - name="name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -150,7 +141,6 @@ def test_path_params_update(self, client: Writer) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `graph_id` but received ''"): client.graphs.with_raw_response.update( graph_id="", - name="name", ) @parametrize @@ -322,24 +312,20 @@ class TestAsyncGraphs: @parametrize async def test_method_create(self, async_client: AsyncWriter) -> None: - graph = await async_client.graphs.create( - name="name", - ) + graph = await async_client.graphs.create() assert_matches_type(GraphCreateResponse, graph, path=["response"]) @parametrize async def test_method_create_with_all_params(self, async_client: AsyncWriter) -> None: graph = await async_client.graphs.create( - name="name", description="description", + name="name", ) assert_matches_type(GraphCreateResponse, graph, path=["response"]) @parametrize async def test_raw_response_create(self, async_client: AsyncWriter) -> None: - response = await async_client.graphs.with_raw_response.create( - name="name", - ) + response = await async_client.graphs.with_raw_response.create() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -348,9 +334,7 @@ async def test_raw_response_create(self, async_client: AsyncWriter) -> None: @parametrize async def test_streaming_response_create(self, async_client: AsyncWriter) -> None: - async with async_client.graphs.with_streaming_response.create( - name="name", - ) as response: + async with async_client.graphs.with_streaming_response.create() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -401,7 +385,6 @@ async def test_path_params_retrieve(self, async_client: AsyncWriter) -> None: async def test_method_update(self, async_client: AsyncWriter) -> None: graph = await async_client.graphs.update( graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - name="name", ) assert_matches_type(GraphUpdateResponse, graph, path=["response"]) @@ -409,8 +392,8 @@ async def test_method_update(self, async_client: AsyncWriter) -> None: async def test_method_update_with_all_params(self, async_client: AsyncWriter) -> None: graph = await async_client.graphs.update( graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - name="name", description="description", + name="name", ) assert_matches_type(GraphUpdateResponse, graph, path=["response"]) @@ -418,7 +401,6 @@ async def test_method_update_with_all_params(self, async_client: AsyncWriter) -> async def test_raw_response_update(self, async_client: AsyncWriter) -> None: response = await async_client.graphs.with_raw_response.update( graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - name="name", ) assert response.is_closed is True @@ -430,7 +412,6 @@ async def test_raw_response_update(self, async_client: AsyncWriter) -> None: async def test_streaming_response_update(self, async_client: AsyncWriter) -> None: async with async_client.graphs.with_streaming_response.update( graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - name="name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -445,7 +426,6 @@ async def test_path_params_update(self, async_client: AsyncWriter) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `graph_id` but received ''"): await async_client.graphs.with_raw_response.update( graph_id="", - name="name", ) @parametrize From b4903408ade4e4a79467e5b3d8a14a4f185a10e9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 8 Aug 2024 13:17:06 +0000 Subject: [PATCH 055/399] chore(internal): codegen related update (#48) --- requirements-dev.lock | 2 +- src/writerai/_base_client.py | 8 +++++ src/writerai/_response.py | 5 +++ tests/test_client.py | 61 ++++++++++++++++++++++++++++++++++++ 4 files changed, 75 insertions(+), 1 deletion(-) diff --git a/requirements-dev.lock b/requirements-dev.lock index d9332cf2..5f5a3d50 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -70,7 +70,7 @@ pydantic-core==2.18.2 # via pydantic pygments==2.18.0 # via rich -pyright==1.1.364 +pyright==1.1.374 pytest==7.1.1 # via pytest-asyncio pytest-asyncio==0.21.1 diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index 11f7e743..fb189bb4 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -1049,6 +1049,7 @@ def _request( response=response, stream=stream, stream_cls=stream_cls, + retries_taken=options.get_max_retries(self.max_retries) - retries, ) def _retry_request( @@ -1090,6 +1091,7 @@ def _process_response( response: httpx.Response, stream: bool, stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, + retries_taken: int = 0, ) -> ResponseT: origin = get_origin(cast_to) or cast_to @@ -1107,6 +1109,7 @@ def _process_response( stream=stream, stream_cls=stream_cls, options=options, + retries_taken=retries_taken, ), ) @@ -1120,6 +1123,7 @@ def _process_response( stream=stream, stream_cls=stream_cls, options=options, + retries_taken=retries_taken, ) if bool(response.request.headers.get(RAW_RESPONSE_HEADER)): return cast(ResponseT, api_response) @@ -1610,6 +1614,7 @@ async def _request( response=response, stream=stream, stream_cls=stream_cls, + retries_taken=options.get_max_retries(self.max_retries) - retries, ) async def _retry_request( @@ -1649,6 +1654,7 @@ async def _process_response( response: httpx.Response, stream: bool, stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, + retries_taken: int = 0, ) -> ResponseT: origin = get_origin(cast_to) or cast_to @@ -1666,6 +1672,7 @@ async def _process_response( stream=stream, stream_cls=stream_cls, options=options, + retries_taken=retries_taken, ), ) @@ -1679,6 +1686,7 @@ async def _process_response( stream=stream, stream_cls=stream_cls, options=options, + retries_taken=retries_taken, ) if bool(response.request.headers.get(RAW_RESPONSE_HEADER)): return cast(ResponseT, api_response) diff --git a/src/writerai/_response.py b/src/writerai/_response.py index f587c71b..b7b9bfb9 100644 --- a/src/writerai/_response.py +++ b/src/writerai/_response.py @@ -55,6 +55,9 @@ class BaseAPIResponse(Generic[R]): http_response: httpx.Response + retries_taken: int + """The number of retries made. If no retries happened this will be `0`""" + def __init__( self, *, @@ -64,6 +67,7 @@ def __init__( stream: bool, stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, options: FinalRequestOptions, + retries_taken: int = 0, ) -> None: self._cast_to = cast_to self._client = client @@ -72,6 +76,7 @@ def __init__( self._stream_cls = stream_cls self._options = options self.http_response = raw + self.retries_taken = retries_taken @property def headers(self) -> httpx.Headers: diff --git a/tests/test_client.py b/tests/test_client.py index c6098095..d1903070 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -763,6 +763,35 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> Non assert _get_open_connections(self.client) == 0 + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("writerai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + def test_retries_taken(self, client: Writer, failures_before_success: int, respx_mock: MockRouter) -> None: + client = client.with_options(max_retries=4) + + nb_retries = 0 + + def retry_handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_retries + if nb_retries < failures_before_success: + nb_retries += 1 + return httpx.Response(500) + return httpx.Response(200) + + respx_mock.post("/v1/chat").mock(side_effect=retry_handler) + + response = client.chat.with_raw_response.chat( + messages=[ + { + "content": "content", + "role": "user", + } + ], + model="model", + ) + + assert response.retries_taken == failures_before_success + class TestAsyncWriter: client = AsyncWriter(base_url=base_url, api_key=api_key, _strict_response_validation=True) @@ -1493,3 +1522,35 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) ) assert _get_open_connections(self.client) == 0 + + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("writerai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + @pytest.mark.asyncio + async def test_retries_taken( + self, async_client: AsyncWriter, failures_before_success: int, respx_mock: MockRouter + ) -> None: + client = async_client.with_options(max_retries=4) + + nb_retries = 0 + + def retry_handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_retries + if nb_retries < failures_before_success: + nb_retries += 1 + return httpx.Response(500) + return httpx.Response(200) + + respx_mock.post("/v1/chat").mock(side_effect=retry_handler) + + response = await client.chat.with_raw_response.chat( + messages=[ + { + "content": "content", + "role": "user", + } + ], + model="model", + ) + + assert response.retries_taken == failures_before_success From b88c8e34c78219a621877a01c641801c166909cb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 8 Aug 2024 13:17:39 +0000 Subject: [PATCH 056/399] chore(internal): test updates (#49) --- src/writerai/_utils/_reflection.py | 2 +- tests/test_client.py | 7 +++++-- tests/utils.py | 10 +++++++--- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/writerai/_utils/_reflection.py b/src/writerai/_utils/_reflection.py index 9a53c7bd..89aa712a 100644 --- a/src/writerai/_utils/_reflection.py +++ b/src/writerai/_utils/_reflection.py @@ -34,7 +34,7 @@ def assert_signatures_in_sync( if custom_param.annotation != source_param.annotation: errors.append( - f"types for the `{name}` param are do not match; source={repr(source_param.annotation)} checking={repr(source_param.annotation)}" + f"types for the `{name}` param are do not match; source={repr(source_param.annotation)} checking={repr(custom_param.annotation)}" ) continue diff --git a/tests/test_client.py b/tests/test_client.py index d1903070..4bf7e0df 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -17,6 +17,7 @@ from pydantic import ValidationError from writerai import Writer, AsyncWriter, APIResponseValidationError +from writerai._types import Omit from writerai._models import BaseModel, FinalRequestOptions from writerai._constants import RAW_RESPONSE_HEADER from writerai._streaming import Stream, AsyncStream @@ -333,7 +334,8 @@ def test_validate_headers(self) -> None: assert request.headers.get("Authorization") == f"Bearer {api_key}" with pytest.raises(WriterError): - client2 = Writer(base_url=base_url, api_key=None, _strict_response_validation=True) + with update_env(**{"WRITER_API_KEY": Omit()}): + client2 = Writer(base_url=base_url, api_key=None, _strict_response_validation=True) _ = client2 def test_default_query_option(self) -> None: @@ -1078,7 +1080,8 @@ def test_validate_headers(self) -> None: assert request.headers.get("Authorization") == f"Bearer {api_key}" with pytest.raises(WriterError): - client2 = AsyncWriter(base_url=base_url, api_key=None, _strict_response_validation=True) + with update_env(**{"WRITER_API_KEY": Omit()}): + client2 = AsyncWriter(base_url=base_url, api_key=None, _strict_response_validation=True) _ = client2 def test_default_query_option(self) -> None: diff --git a/tests/utils.py b/tests/utils.py index 683796ce..741b4af6 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -8,7 +8,7 @@ from datetime import date, datetime from typing_extensions import Literal, get_args, get_origin, assert_type -from writerai._types import NoneType +from writerai._types import Omit, NoneType from writerai._utils import ( is_dict, is_list, @@ -139,11 +139,15 @@ def _assert_list_type(type_: type[object], value: object) -> None: @contextlib.contextmanager -def update_env(**new_env: str) -> Iterator[None]: +def update_env(**new_env: str | Omit) -> Iterator[None]: old = os.environ.copy() try: - os.environ.update(new_env) + for name, value in new_env.items(): + if isinstance(value, Omit): + os.environ.pop(name, None) + else: + os.environ[name] = value yield None finally: From 22a9740f3fc237ec4f9965a98253c198faa4a710 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 8 Aug 2024 13:20:07 +0000 Subject: [PATCH 057/399] chore(internal): bump ruff version (#50) --- pyproject.toml | 12 ++++--- requirements-dev.lock | 2 +- src/writerai/_base_client.py | 63 +++++++++++---------------------- src/writerai/_compat.py | 24 +++++-------- src/writerai/_files.py | 12 +++---- src/writerai/_response.py | 12 +++---- src/writerai/_types.py | 9 ++--- src/writerai/_utils/_proxy.py | 3 +- src/writerai/_utils/_utils.py | 18 ++++------ tests/test_deepcopy.py | 3 +- tests/test_response.py | 12 +++---- tests/test_utils/test_typing.py | 15 +++----- 12 files changed, 65 insertions(+), 120 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c2ef6d41..122514d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,8 +77,8 @@ format = { chain = [ "check:ruff", "typecheck", ]} -"check:ruff" = "ruff ." -"fix:ruff" = "ruff --fix ." +"check:ruff" = "ruff check ." +"fix:ruff" = "ruff check --fix ." typecheck = { chain = [ "typecheck:pyright", @@ -162,6 +162,11 @@ reportPrivateUsage = false line-length = 120 output-format = "grouped" target-version = "py37" + +[tool.ruff.format] +docstring-code-format = true + +[tool.ruff.lint] select = [ # isort "I", @@ -192,9 +197,6 @@ unfixable = [ ] ignore-init-module-imports = true -[tool.ruff.format] -docstring-code-format = true - [tool.ruff.lint.flake8-tidy-imports.banned-api] "functools.lru_cache".msg = "This function does not retain type information for the wrapped function's arguments; The `lru_cache` function from `_utils` should be used instead" diff --git a/requirements-dev.lock b/requirements-dev.lock index 5f5a3d50..baa25f20 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -80,7 +80,7 @@ pytz==2023.3.post1 # via dirty-equals respx==0.20.2 rich==13.7.1 -ruff==0.1.9 +ruff==0.5.6 setuptools==68.2.2 # via nodeenv six==1.16.0 diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index fb189bb4..235829d3 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -124,16 +124,14 @@ def __init__( self, *, url: URL, - ) -> None: - ... + ) -> None: ... @overload def __init__( self, *, params: Query, - ) -> None: - ... + ) -> None: ... def __init__( self, @@ -166,8 +164,7 @@ def has_next_page(self) -> bool: return False return self.next_page_info() is not None - def next_page_info(self) -> Optional[PageInfo]: - ... + def next_page_info(self) -> Optional[PageInfo]: ... def _get_page_items(self) -> Iterable[_T]: # type: ignore[empty-body] ... @@ -903,8 +900,7 @@ def request( *, stream: Literal[True], stream_cls: Type[_StreamT], - ) -> _StreamT: - ... + ) -> _StreamT: ... @overload def request( @@ -914,8 +910,7 @@ def request( remaining_retries: Optional[int] = None, *, stream: Literal[False] = False, - ) -> ResponseT: - ... + ) -> ResponseT: ... @overload def request( @@ -926,8 +921,7 @@ def request( *, stream: bool = False, stream_cls: Type[_StreamT] | None = None, - ) -> ResponseT | _StreamT: - ... + ) -> ResponseT | _StreamT: ... def request( self, @@ -1156,8 +1150,7 @@ def get( cast_to: Type[ResponseT], options: RequestOptions = {}, stream: Literal[False] = False, - ) -> ResponseT: - ... + ) -> ResponseT: ... @overload def get( @@ -1168,8 +1161,7 @@ def get( options: RequestOptions = {}, stream: Literal[True], stream_cls: type[_StreamT], - ) -> _StreamT: - ... + ) -> _StreamT: ... @overload def get( @@ -1180,8 +1172,7 @@ def get( options: RequestOptions = {}, stream: bool, stream_cls: type[_StreamT] | None = None, - ) -> ResponseT | _StreamT: - ... + ) -> ResponseT | _StreamT: ... def get( self, @@ -1207,8 +1198,7 @@ def post( options: RequestOptions = {}, files: RequestFiles | None = None, stream: Literal[False] = False, - ) -> ResponseT: - ... + ) -> ResponseT: ... @overload def post( @@ -1221,8 +1211,7 @@ def post( files: RequestFiles | None = None, stream: Literal[True], stream_cls: type[_StreamT], - ) -> _StreamT: - ... + ) -> _StreamT: ... @overload def post( @@ -1235,8 +1224,7 @@ def post( files: RequestFiles | None = None, stream: bool, stream_cls: type[_StreamT] | None = None, - ) -> ResponseT | _StreamT: - ... + ) -> ResponseT | _StreamT: ... def post( self, @@ -1469,8 +1457,7 @@ async def request( *, stream: Literal[False] = False, remaining_retries: Optional[int] = None, - ) -> ResponseT: - ... + ) -> ResponseT: ... @overload async def request( @@ -1481,8 +1468,7 @@ async def request( stream: Literal[True], stream_cls: type[_AsyncStreamT], remaining_retries: Optional[int] = None, - ) -> _AsyncStreamT: - ... + ) -> _AsyncStreamT: ... @overload async def request( @@ -1493,8 +1479,7 @@ async def request( stream: bool, stream_cls: type[_AsyncStreamT] | None = None, remaining_retries: Optional[int] = None, - ) -> ResponseT | _AsyncStreamT: - ... + ) -> ResponseT | _AsyncStreamT: ... async def request( self, @@ -1709,8 +1694,7 @@ async def get( cast_to: Type[ResponseT], options: RequestOptions = {}, stream: Literal[False] = False, - ) -> ResponseT: - ... + ) -> ResponseT: ... @overload async def get( @@ -1721,8 +1705,7 @@ async def get( options: RequestOptions = {}, stream: Literal[True], stream_cls: type[_AsyncStreamT], - ) -> _AsyncStreamT: - ... + ) -> _AsyncStreamT: ... @overload async def get( @@ -1733,8 +1716,7 @@ async def get( options: RequestOptions = {}, stream: bool, stream_cls: type[_AsyncStreamT] | None = None, - ) -> ResponseT | _AsyncStreamT: - ... + ) -> ResponseT | _AsyncStreamT: ... async def get( self, @@ -1758,8 +1740,7 @@ async def post( files: RequestFiles | None = None, options: RequestOptions = {}, stream: Literal[False] = False, - ) -> ResponseT: - ... + ) -> ResponseT: ... @overload async def post( @@ -1772,8 +1753,7 @@ async def post( options: RequestOptions = {}, stream: Literal[True], stream_cls: type[_AsyncStreamT], - ) -> _AsyncStreamT: - ... + ) -> _AsyncStreamT: ... @overload async def post( @@ -1786,8 +1766,7 @@ async def post( options: RequestOptions = {}, stream: bool, stream_cls: type[_AsyncStreamT] | None = None, - ) -> ResponseT | _AsyncStreamT: - ... + ) -> ResponseT | _AsyncStreamT: ... async def post( self, diff --git a/src/writerai/_compat.py b/src/writerai/_compat.py index c919b5ad..7c6f91a8 100644 --- a/src/writerai/_compat.py +++ b/src/writerai/_compat.py @@ -159,22 +159,19 @@ def model_parse(model: type[_ModelT], data: Any) -> _ModelT: # generic models if TYPE_CHECKING: - class GenericModel(pydantic.BaseModel): - ... + class GenericModel(pydantic.BaseModel): ... else: if PYDANTIC_V2: # there no longer needs to be a distinction in v2 but # we still have to create our own subclass to avoid # inconsistent MRO ordering errors - class GenericModel(pydantic.BaseModel): - ... + class GenericModel(pydantic.BaseModel): ... else: import pydantic.generics - class GenericModel(pydantic.generics.GenericModel, pydantic.BaseModel): - ... + class GenericModel(pydantic.generics.GenericModel, pydantic.BaseModel): ... # cached properties @@ -193,26 +190,21 @@ class typed_cached_property(Generic[_T]): func: Callable[[Any], _T] attrname: str | None - def __init__(self, func: Callable[[Any], _T]) -> None: - ... + def __init__(self, func: Callable[[Any], _T]) -> None: ... @overload - def __get__(self, instance: None, owner: type[Any] | None = None) -> Self: - ... + def __get__(self, instance: None, owner: type[Any] | None = None) -> Self: ... @overload - def __get__(self, instance: object, owner: type[Any] | None = None) -> _T: - ... + def __get__(self, instance: object, owner: type[Any] | None = None) -> _T: ... def __get__(self, instance: object, owner: type[Any] | None = None) -> _T | Self: raise NotImplementedError() - def __set_name__(self, owner: type[Any], name: str) -> None: - ... + def __set_name__(self, owner: type[Any], name: str) -> None: ... # __set__ is not defined at runtime, but @cached_property is designed to be settable - def __set__(self, instance: object, value: _T) -> None: - ... + def __set__(self, instance: object, value: _T) -> None: ... else: try: from functools import cached_property as cached_property diff --git a/src/writerai/_files.py b/src/writerai/_files.py index 0d2022ae..715cc207 100644 --- a/src/writerai/_files.py +++ b/src/writerai/_files.py @@ -39,13 +39,11 @@ def assert_is_file_content(obj: object, *, key: str | None = None) -> None: @overload -def to_httpx_files(files: None) -> None: - ... +def to_httpx_files(files: None) -> None: ... @overload -def to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: - ... +def to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: ... def to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles | None: @@ -83,13 +81,11 @@ def _read_file_content(file: FileContent) -> HttpxFileContent: @overload -async def async_to_httpx_files(files: None) -> None: - ... +async def async_to_httpx_files(files: None) -> None: ... @overload -async def async_to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: - ... +async def async_to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: ... async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles | None: diff --git a/src/writerai/_response.py b/src/writerai/_response.py index b7b9bfb9..f6917b19 100644 --- a/src/writerai/_response.py +++ b/src/writerai/_response.py @@ -260,12 +260,10 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: class APIResponse(BaseAPIResponse[R]): @overload - def parse(self, *, to: type[_T]) -> _T: - ... + def parse(self, *, to: type[_T]) -> _T: ... @overload - def parse(self) -> R: - ... + def parse(self) -> R: ... def parse(self, *, to: type[_T] | None = None) -> R | _T: """Returns the rich python representation of this response's data. @@ -364,12 +362,10 @@ def iter_lines(self) -> Iterator[str]: class AsyncAPIResponse(BaseAPIResponse[R]): @overload - async def parse(self, *, to: type[_T]) -> _T: - ... + async def parse(self, *, to: type[_T]) -> _T: ... @overload - async def parse(self) -> R: - ... + async def parse(self) -> R: ... async def parse(self, *, to: type[_T] | None = None) -> R | _T: """Returns the rich python representation of this response's data. diff --git a/src/writerai/_types.py b/src/writerai/_types.py index 7dbbaa54..35538cb8 100644 --- a/src/writerai/_types.py +++ b/src/writerai/_types.py @@ -111,8 +111,7 @@ class NotGiven: For example: ```py - def get(timeout: Union[int, NotGiven, None] = NotGiven()) -> Response: - ... + def get(timeout: Union[int, NotGiven, None] = NotGiven()) -> Response: ... get(timeout=1) # 1s timeout @@ -162,16 +161,14 @@ def build( *, response: Response, data: object, - ) -> _T: - ... + ) -> _T: ... Headers = Mapping[str, Union[str, Omit]] class HeadersLikeProtocol(Protocol): - def get(self, __key: str) -> str | None: - ... + def get(self, __key: str) -> str | None: ... HeadersLike = Union[Headers, HeadersLikeProtocol] diff --git a/src/writerai/_utils/_proxy.py b/src/writerai/_utils/_proxy.py index c46a62a6..ffd883e9 100644 --- a/src/writerai/_utils/_proxy.py +++ b/src/writerai/_utils/_proxy.py @@ -59,5 +59,4 @@ def __as_proxied__(self) -> T: return cast(T, self) @abstractmethod - def __load__(self) -> T: - ... + def __load__(self) -> T: ... diff --git a/src/writerai/_utils/_utils.py b/src/writerai/_utils/_utils.py index 34797c29..2fc5a1c6 100644 --- a/src/writerai/_utils/_utils.py +++ b/src/writerai/_utils/_utils.py @@ -211,20 +211,17 @@ def required_args(*variants: Sequence[str]) -> Callable[[CallableT], CallableT]: Example usage: ```py @overload - def foo(*, a: str) -> str: - ... + def foo(*, a: str) -> str: ... @overload - def foo(*, b: bool) -> str: - ... + def foo(*, b: bool) -> str: ... # This enforces the same constraints that a static type checker would # i.e. that either a or b must be passed to the function @required_args(["a"], ["b"]) - def foo(*, a: str | None = None, b: bool | None = None) -> str: - ... + def foo(*, a: str | None = None, b: bool | None = None) -> str: ... ``` """ @@ -286,18 +283,15 @@ def wrapper(*args: object, **kwargs: object) -> object: @overload -def strip_not_given(obj: None) -> None: - ... +def strip_not_given(obj: None) -> None: ... @overload -def strip_not_given(obj: Mapping[_K, _V | NotGiven]) -> dict[_K, _V]: - ... +def strip_not_given(obj: Mapping[_K, _V | NotGiven]) -> dict[_K, _V]: ... @overload -def strip_not_given(obj: object) -> object: - ... +def strip_not_given(obj: object) -> object: ... def strip_not_given(obj: object | None) -> object: diff --git a/tests/test_deepcopy.py b/tests/test_deepcopy.py index 628e4049..51522e9b 100644 --- a/tests/test_deepcopy.py +++ b/tests/test_deepcopy.py @@ -41,8 +41,7 @@ def test_nested_list() -> None: assert_different_identities(obj1[1], obj2[1]) -class MyObject: - ... +class MyObject: ... def test_ignores_other_types() -> None: diff --git a/tests/test_response.py b/tests/test_response.py index 0a4473fd..c9bf10cb 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -19,16 +19,13 @@ from writerai._base_client import FinalRequestOptions -class ConcreteBaseAPIResponse(APIResponse[bytes]): - ... +class ConcreteBaseAPIResponse(APIResponse[bytes]): ... -class ConcreteAPIResponse(APIResponse[List[str]]): - ... +class ConcreteAPIResponse(APIResponse[List[str]]): ... -class ConcreteAsyncAPIResponse(APIResponse[httpx.Response]): - ... +class ConcreteAsyncAPIResponse(APIResponse[httpx.Response]): ... def test_extract_response_type_direct_classes() -> None: @@ -56,8 +53,7 @@ def test_extract_response_type_binary_response() -> None: assert extract_response_type(AsyncBinaryAPIResponse) == bytes -class PydanticModel(pydantic.BaseModel): - ... +class PydanticModel(pydantic.BaseModel): ... def test_response_parse_mismatched_basemodel(client: Writer) -> None: diff --git a/tests/test_utils/test_typing.py b/tests/test_utils/test_typing.py index e0a437df..8237ec21 100644 --- a/tests/test_utils/test_typing.py +++ b/tests/test_utils/test_typing.py @@ -9,24 +9,19 @@ _T3 = TypeVar("_T3") -class BaseGeneric(Generic[_T]): - ... +class BaseGeneric(Generic[_T]): ... -class SubclassGeneric(BaseGeneric[_T]): - ... +class SubclassGeneric(BaseGeneric[_T]): ... -class BaseGenericMultipleTypeArgs(Generic[_T, _T2, _T3]): - ... +class BaseGenericMultipleTypeArgs(Generic[_T, _T2, _T3]): ... -class SubclassGenericMultipleTypeArgs(BaseGenericMultipleTypeArgs[_T, _T2, _T3]): - ... +class SubclassGenericMultipleTypeArgs(BaseGenericMultipleTypeArgs[_T, _T2, _T3]): ... -class SubclassDifferentOrderGenericMultipleTypeArgs(BaseGenericMultipleTypeArgs[_T2, _T, _T3]): - ... +class SubclassDifferentOrderGenericMultipleTypeArgs(BaseGenericMultipleTypeArgs[_T2, _T, _T3]): ... def test_extract_type_var() -> None: From 20342d3b0563a062a1b68c52acfc2897fb657098 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 8 Aug 2024 13:21:08 +0000 Subject: [PATCH 058/399] chore(internal): update pydantic compat helper function (#51) --- src/writerai/_compat.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/writerai/_compat.py b/src/writerai/_compat.py index 7c6f91a8..21fe6941 100644 --- a/src/writerai/_compat.py +++ b/src/writerai/_compat.py @@ -7,7 +7,7 @@ import pydantic from pydantic.fields import FieldInfo -from ._types import StrBytesIntFloat +from ._types import IncEx, StrBytesIntFloat _T = TypeVar("_T") _ModelT = TypeVar("_ModelT", bound=pydantic.BaseModel) @@ -133,17 +133,20 @@ def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str: def model_dump( model: pydantic.BaseModel, *, + exclude: IncEx = None, exclude_unset: bool = False, exclude_defaults: bool = False, ) -> dict[str, Any]: if PYDANTIC_V2: return model.model_dump( + exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, ) return cast( "dict[str, Any]", model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast] + exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, ), From a6d28a94360400c16428b85c6c3383e41d8dacee Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 8 Aug 2024 13:27:15 +0000 Subject: [PATCH 059/399] chore(internal): remove deprecated ruff config (#52) --- pyproject.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 122514d0..6c192266 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -195,7 +195,6 @@ unfixable = [ "T201", "T203", ] -ignore-init-module-imports = true [tool.ruff.lint.flake8-tidy-imports.banned-api] "functools.lru_cache".msg = "This function does not retain type information for the wrapped function's arguments; The `lru_cache` function from `_utils` should be used instead" @@ -207,7 +206,7 @@ combine-as-imports = true extra-standard-library = ["typing_extensions"] known-first-party = ["writerai", "tests"] -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] "bin/**.py" = ["T201", "T203"] "scripts/**.py" = ["T201", "T203"] "tests/**.py" = ["T201", "T203"] From 8b1a3d0a017ba6363e0af8cefcfab0e935ca9239 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 14 Aug 2024 09:57:45 +0000 Subject: [PATCH 060/399] docs(api): updates to API spec (#53) --- .stats.yml | 2 +- src/writerai/resources/files.py | 4 ++-- .../types/application_generate_content_params.py | 12 ++++++++++++ .../types/application_generate_content_response.py | 2 ++ 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index 789612fa..7cfc3fc1 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 16 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-4a89aa730a0c5dad44fa86cfa4f14b10fa44a076dfe6d5d6a2eb1cf8569bf526.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-b0de8fe3df00ba634727ad67463d6e2163588f28ce241f4ea45ccff2a25c271f.yml diff --git a/src/writerai/resources/files.py b/src/writerai/resources/files.py index 5ce78ad2..ee3b20ed 100644 --- a/src/writerai/resources/files.py +++ b/src/writerai/resources/files.py @@ -200,7 +200,7 @@ def download( """ if not file_id: raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") - extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} + extra_headers = {"Accept": "file contents", **(extra_headers or {})} return self._get( f"/v1/files/{file_id}/download", options=make_request_options( @@ -413,7 +413,7 @@ async def download( """ if not file_id: raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") - extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} + extra_headers = {"Accept": "file contents", **(extra_headers or {})} return await self._get( f"/v1/files/{file_id}/download", options=make_request_options( diff --git a/src/writerai/types/application_generate_content_params.py b/src/writerai/types/application_generate_content_params.py index 5ef75a50..2b93046b 100644 --- a/src/writerai/types/application_generate_content_params.py +++ b/src/writerai/types/application_generate_content_params.py @@ -14,5 +14,17 @@ class ApplicationGenerateContentParams(TypedDict, total=False): class Input(TypedDict, total=False): id: Required[str] + """The unique identifier for the input field from the application. + + All input types from the No-code application are supported (i.e. Text input, + Dropdown, File upload, Image input). The identifier should be the name of the + input type. + """ value: Required[List[str]] + """The value for the input field. + + If file is required you will need to pass a `file_id`. See + [here](https://dev.writer.com/api-guides/api-reference/file-api/upload-files) + for the Files API. + """ diff --git a/src/writerai/types/application_generate_content_response.py b/src/writerai/types/application_generate_content_response.py index 5f9e8572..240a6a82 100644 --- a/src/writerai/types/application_generate_content_response.py +++ b/src/writerai/types/application_generate_content_response.py @@ -9,5 +9,7 @@ class ApplicationGenerateContentResponse(BaseModel): suggestion: str + """The response from the model specified in the application.""" title: Optional[str] = None + """The name of the output field.""" From 40ab49f08bdedf9b9b3e59b4af5395f09f1e6328 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 14 Aug 2024 09:59:01 +0000 Subject: [PATCH 061/399] chore(ci): bump prism mock server version (#54) --- scripts/mock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/mock b/scripts/mock index f5861576..d2814ae6 100755 --- a/scripts/mock +++ b/scripts/mock @@ -21,7 +21,7 @@ echo "==> Starting mock server with URL ${URL}" # Run prism mock on the given spec if [ "$1" == "--daemon" ]; then - npm exec --package=@stainless-api/prism-cli@5.8.4 -- prism mock "$URL" &> .prism.log & + npm exec --package=@stainless-api/prism-cli@5.8.5 -- prism mock "$URL" &> .prism.log & # Wait for server to come online echo -n "Waiting for server" @@ -37,5 +37,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stainless-api/prism-cli@5.8.4 -- prism mock "$URL" + npm exec --package=@stainless-api/prism-cli@5.8.5 -- prism mock "$URL" fi From 1556c8bbc2e3c8fb71585a3c8647cae4a7cfd705 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 14 Aug 2024 09:59:46 +0000 Subject: [PATCH 062/399] chore(internal): ensure package is importable in lint cmd (#55) --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 6c192266..1dac3c1c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,10 +76,13 @@ format = { chain = [ "lint" = { chain = [ "check:ruff", "typecheck", + "check:importable", ]} "check:ruff" = "ruff check ." "fix:ruff" = "ruff check --fix ." +"check:importable" = "python -c 'import writerai'" + typecheck = { chain = [ "typecheck:pyright", "typecheck:mypy" From bac7ea3515612fed70b7b6c0336c7233909fd1c1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 14 Aug 2024 10:03:19 +0000 Subject: [PATCH 063/399] chore(examples): minor formatting changes (#56) --- tests/api_resources/test_completions.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/api_resources/test_completions.py b/tests/api_resources/test_completions.py index 428bab7d..4db055fa 100644 --- a/tests/api_resources/test_completions.py +++ b/tests/api_resources/test_completions.py @@ -71,7 +71,7 @@ def test_method_create_overload_2(self, client: Writer) -> None: completion_stream = client.completions.create( model="palmyra-x-002-instruct", prompt="Write me an SEO article about...", - stream=False, + stream=True, ) completion_stream.response.close() @@ -80,7 +80,7 @@ def test_method_create_with_all_params_overload_2(self, client: Writer) -> None: completion_stream = client.completions.create( model="palmyra-x-002-instruct", prompt="Write me an SEO article about...", - stream=False, + stream=True, best_of=1, max_tokens=150, random_seed=42, @@ -95,7 +95,7 @@ def test_raw_response_create_overload_2(self, client: Writer) -> None: response = client.completions.with_raw_response.create( model="palmyra-x-002-instruct", prompt="Write me an SEO article about...", - stream=False, + stream=True, ) assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -107,7 +107,7 @@ def test_streaming_response_create_overload_2(self, client: Writer) -> None: with client.completions.with_streaming_response.create( model="palmyra-x-002-instruct", prompt="Write me an SEO article about...", - stream=False, + stream=True, ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -175,7 +175,7 @@ async def test_method_create_overload_2(self, async_client: AsyncWriter) -> None completion_stream = await async_client.completions.create( model="palmyra-x-002-instruct", prompt="Write me an SEO article about...", - stream=False, + stream=True, ) await completion_stream.response.aclose() @@ -184,7 +184,7 @@ async def test_method_create_with_all_params_overload_2(self, async_client: Asyn completion_stream = await async_client.completions.create( model="palmyra-x-002-instruct", prompt="Write me an SEO article about...", - stream=False, + stream=True, best_of=1, max_tokens=150, random_seed=42, @@ -199,7 +199,7 @@ async def test_raw_response_create_overload_2(self, async_client: AsyncWriter) - response = await async_client.completions.with_raw_response.create( model="palmyra-x-002-instruct", prompt="Write me an SEO article about...", - stream=False, + stream=True, ) assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -211,7 +211,7 @@ async def test_streaming_response_create_overload_2(self, async_client: AsyncWri async with async_client.completions.with_streaming_response.create( model="palmyra-x-002-instruct", prompt="Write me an SEO article about...", - stream=False, + stream=True, ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" From 52660b43db1dbdc8a809bd1d5f0f45ca59204231 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 14 Aug 2024 10:05:35 +0000 Subject: [PATCH 064/399] chore(internal): codegen related update (#57) --- src/writerai/resources/files.py | 14 ++------------ src/writerai/types/file_upload_params.py | 2 -- tests/api_resources/test_files.py | 6 ------ 3 files changed, 2 insertions(+), 20 deletions(-) diff --git a/src/writerai/resources/files.py b/src/writerai/resources/files.py index ee3b20ed..83e29a0e 100644 --- a/src/writerai/resources/files.py +++ b/src/writerai/resources/files.py @@ -214,7 +214,6 @@ def upload( *, content: FileTypes, content_disposition: str, - content_type: str, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -234,11 +233,7 @@ def upload( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = { - "Content-Disposition": content_disposition, - "Content-Type": content_type, - **(extra_headers or {}), - } + extra_headers = {"Content-Disposition": content_disposition, **(extra_headers or {})} return self._post( "/v1/files", body=maybe_transform(content, file_upload_params.FileUploadParams), @@ -427,7 +422,6 @@ async def upload( *, content: FileTypes, content_disposition: str, - content_type: str, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -447,11 +441,7 @@ async def upload( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = { - "Content-Disposition": content_disposition, - "Content-Type": content_type, - **(extra_headers or {}), - } + extra_headers = {"Content-Disposition": content_disposition, **(extra_headers or {})} return await self._post( "/v1/files", body=await async_maybe_transform(content, file_upload_params.FileUploadParams), diff --git a/src/writerai/types/file_upload_params.py b/src/writerai/types/file_upload_params.py index 14077b97..760021f9 100644 --- a/src/writerai/types/file_upload_params.py +++ b/src/writerai/types/file_upload_params.py @@ -14,5 +14,3 @@ class FileUploadParams(TypedDict, total=False): content: Required[FileTypes] content_disposition: Required[Annotated[str, PropertyInfo(alias="Content-Disposition")]] - - content_type: Required[Annotated[str, PropertyInfo(alias="Content-Type")]] diff --git a/tests/api_resources/test_files.py b/tests/api_resources/test_files.py index 6e022684..85041dcf 100644 --- a/tests/api_resources/test_files.py +++ b/tests/api_resources/test_files.py @@ -198,7 +198,6 @@ def test_method_upload(self, client: Writer) -> None: file = client.files.upload( content=b"raw file contents", content_disposition="Content-Disposition", - content_type="Content-Type", ) assert_matches_type(File, file, path=["response"]) @@ -208,7 +207,6 @@ def test_raw_response_upload(self, client: Writer) -> None: response = client.files.with_raw_response.upload( content=b"raw file contents", content_disposition="Content-Disposition", - content_type="Content-Type", ) assert response.is_closed is True @@ -222,7 +220,6 @@ def test_streaming_response_upload(self, client: Writer) -> None: with client.files.with_streaming_response.upload( content=b"raw file contents", content_disposition="Content-Disposition", - content_type="Content-Type", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -408,7 +405,6 @@ async def test_method_upload(self, async_client: AsyncWriter) -> None: file = await async_client.files.upload( content=b"raw file contents", content_disposition="Content-Disposition", - content_type="Content-Type", ) assert_matches_type(File, file, path=["response"]) @@ -418,7 +414,6 @@ async def test_raw_response_upload(self, async_client: AsyncWriter) -> None: response = await async_client.files.with_raw_response.upload( content=b"raw file contents", content_disposition="Content-Disposition", - content_type="Content-Type", ) assert response.is_closed is True @@ -432,7 +427,6 @@ async def test_streaming_response_upload(self, async_client: AsyncWriter) -> Non async with async_client.files.with_streaming_response.upload( content=b"raw file contents", content_disposition="Content-Disposition", - content_type="Content-Type", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" From 73763aa0042b55e3d5718bd9c0b51c8a3c4ec539 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 14 Aug 2024 10:07:24 +0000 Subject: [PATCH 065/399] docs(api): updates to API spec (#58) --- .stats.yml | 2 +- src/writerai/resources/files.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index 7cfc3fc1..e63f5224 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 16 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-b0de8fe3df00ba634727ad67463d6e2163588f28ce241f4ea45ccff2a25c271f.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-29d74057f6277b8657539fbdda86e469e2d50765733bfc3db2578eb6fdb8d07e.yml diff --git a/src/writerai/resources/files.py b/src/writerai/resources/files.py index 83e29a0e..a8b13cff 100644 --- a/src/writerai/resources/files.py +++ b/src/writerai/resources/files.py @@ -200,7 +200,7 @@ def download( """ if not file_id: raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") - extra_headers = {"Accept": "file contents", **(extra_headers or {})} + extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} return self._get( f"/v1/files/{file_id}/download", options=make_request_options( @@ -408,7 +408,7 @@ async def download( """ if not file_id: raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") - extra_headers = {"Accept": "file contents", **(extra_headers or {})} + extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} return await self._get( f"/v1/files/{file_id}/download", options=make_request_options( From 1611465047c5fd278a93d7f27a6c76180a03a057 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 14 Aug 2024 13:54:24 +0000 Subject: [PATCH 066/399] chore(internal): version bump (#59) --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 2aca35ae..4208b5cb 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.5.0" + ".": "0.6.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 1dac3c1c..9a309704 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "0.5.0" +version = "0.6.0" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index 0f5f2c5c..b35f96f3 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "0.5.0" # x-release-please-version +__version__ = "0.6.0" # x-release-please-version From 47cb0b65e54622682ed5911f895e9cf973f896dd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 6 Sep 2024 07:44:21 +0000 Subject: [PATCH 067/399] chore(internal): codegen related update (#60) --- scripts/test | 3 +++ src/writerai/_base_client.py | 4 ++-- src/writerai/_models.py | 2 ++ tests/test_response.py | 39 +++++++++++++++++++++++++++++++++++- 4 files changed, 45 insertions(+), 3 deletions(-) diff --git a/scripts/test b/scripts/test index b3ace901..4fa5698b 100755 --- a/scripts/test +++ b/scripts/test @@ -54,3 +54,6 @@ fi echo "==> Running tests" rye run pytest "$@" + +echo "==> Running Pydantic v1 tests" +rye run nox -s test-pydantic-v1 -- "$@" diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index 235829d3..35ef4b62 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -1,5 +1,6 @@ from __future__ import annotations +import sys import json import time import uuid @@ -1982,7 +1983,6 @@ def get_python_version() -> str: def get_architecture() -> Arch: try: - python_bitness, _ = platform.architecture() machine = platform.machine().lower() except Exception: return "unknown" @@ -1998,7 +1998,7 @@ def get_architecture() -> Arch: return "x64" # TODO: untested - if python_bitness == "32bit": + if sys.maxsize <= 2**32: return "x32" if machine: diff --git a/src/writerai/_models.py b/src/writerai/_models.py index 5148d5a7..d386eaa3 100644 --- a/src/writerai/_models.py +++ b/src/writerai/_models.py @@ -380,6 +380,8 @@ def is_basemodel(type_: type) -> bool: def is_basemodel_type(type_: type) -> TypeGuard[type[BaseModel] | type[GenericModel]]: origin = get_origin(type_) or type_ + if not inspect.isclass(origin): + return False return issubclass(origin, BaseModel) or issubclass(origin, GenericModel) diff --git a/tests/test_response.py b/tests/test_response.py index c9bf10cb..1a496847 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -1,5 +1,5 @@ import json -from typing import List, cast +from typing import Any, List, Union, cast from typing_extensions import Annotated import httpx @@ -188,3 +188,40 @@ async def test_async_response_parse_annotated_type(async_client: AsyncWriter) -> ) assert obj.foo == "hello!" assert obj.bar == 2 + + +class OtherModel(BaseModel): + a: str + + +@pytest.mark.parametrize("client", [False], indirect=True) # loose validation +def test_response_parse_expect_model_union_non_json_content(client: Writer) -> None: + response = APIResponse( + raw=httpx.Response(200, content=b"foo", headers={"Content-Type": "application/text"}), + client=client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + obj = response.parse(to=cast(Any, Union[CustomModel, OtherModel])) + assert isinstance(obj, str) + assert obj == "foo" + + +@pytest.mark.asyncio +@pytest.mark.parametrize("async_client", [False], indirect=True) # loose validation +async def test_async_response_parse_expect_model_union_non_json_content(async_client: AsyncWriter) -> None: + response = AsyncAPIResponse( + raw=httpx.Response(200, content=b"foo", headers={"Content-Type": "application/text"}), + client=async_client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + obj = await response.parse(to=cast(Any, Union[CustomModel, OtherModel])) + assert isinstance(obj, str) + assert obj == "foo" From 00e0c72ab0e144abab3f5eaa9f8151d32967a939 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 6 Sep 2024 07:52:48 +0000 Subject: [PATCH 068/399] chore: pyproject.toml formatting changes (#62) --- pyproject.toml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9a309704..34e265ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,6 @@ dependencies = [ "distro>=1.7.0, <2", "sniffio", "cached-property; python_version < '3.8'", - ] requires-python = ">= 3.7" classifiers = [ @@ -36,8 +35,6 @@ classifiers = [ "License :: OSI Approved :: Apache Software License" ] - - [project.urls] Homepage = "https://github.com/writer/writer-python" Repository = "https://github.com/writer/writer-python" @@ -59,7 +56,6 @@ dev-dependencies = [ "dirty-equals>=0.6.0", "importlib-metadata>=6.7.0", "rich>=13.7.1", - ] [tool.rye.scripts] From 08083f79d84cf2b0c25d9f806047019df61390c9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 6 Sep 2024 08:02:33 +0000 Subject: [PATCH 069/399] chore(internal): version bump (#63) --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 4208b5cb..ac031714 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.6.0" + ".": "0.6.1" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 34e265ce..ac12f3e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "0.6.0" +version = "0.6.1" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index b35f96f3..7d5db693 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "0.6.0" # x-release-please-version +__version__ = "0.6.1" # x-release-please-version From 57c3b86c4c13f518ccfba51eabc36c80218425a6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Sep 2024 17:19:55 +0000 Subject: [PATCH 070/399] docs(api): updates to API spec (#64) --- .stats.yml | 2 +- README.md | 14 +- src/writerai/resources/chat.py | 74 +++++++++- src/writerai/resources/files.py | 10 ++ src/writerai/types/chat.py | 24 +++- src/writerai/types/chat_chat_params.py | 54 +++++++- src/writerai/types/file.py | 3 + src/writerai/types/file_list_params.py | 6 + tests/api_resources/test_chat.py | 172 +++++++++++++++++++----- tests/api_resources/test_completions.py | 32 ++--- tests/api_resources/test_files.py | 2 + tests/test_client.py | 16 +-- 12 files changed, 333 insertions(+), 76 deletions(-) diff --git a/.stats.yml b/.stats.yml index e63f5224..5ca4b6e0 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 16 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-29d74057f6277b8657539fbdda86e469e2d50765733bfc3db2578eb6fdb8d07e.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-149f74d46fcb6e37bb59c571100cf81230cf567855570f301cbb2d18d3a84764.yml diff --git a/README.md b/README.md index f09d90b5..a933bdb0 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ client = Writer( chat = client.chat.chat( messages=[ { - "content": "content", + "content": "Write a memo summarizing this earnings report.", "role": "user", } ], @@ -68,7 +68,7 @@ async def main() -> None: chat = await client.chat.chat( messages=[ { - "content": "content", + "content": "Write a memo summarizing this earnings report.", "role": "user", } ], @@ -144,7 +144,7 @@ try: client.chat.chat( messages=[ { - "content": "content", + "content": "Write a memo summarizing this earnings report.", "role": "user", } ], @@ -195,7 +195,7 @@ client = Writer( client.with_options(max_retries=5).chat.chat( messages=[ { - "content": "content", + "content": "Write a memo summarizing this earnings report.", "role": "user", } ], @@ -226,7 +226,7 @@ client = Writer( client.with_options(timeout=5.0).chat.chat( messages=[ { - "content": "content", + "content": "Write a memo summarizing this earnings report.", "role": "user", } ], @@ -272,7 +272,7 @@ from writerai import Writer client = Writer() response = client.chat.with_raw_response.chat( messages=[{ - "content": "content", + "content": "Write a memo summarizing this earnings report.", "role": "user", }], model="palmyra-x-002-32k", @@ -297,7 +297,7 @@ To stream the response body, use `.with_streaming_response` instead, which requi with client.chat.with_streaming_response.chat( messages=[ { - "content": "content", + "content": "Write a memo summarizing this earnings report.", "role": "user", } ], diff --git a/src/writerai/resources/chat.py b/src/writerai/resources/chat.py index 702651e0..4d13ab81 100644 --- a/src/writerai/resources/chat.py +++ b/src/writerai/resources/chat.py @@ -50,6 +50,8 @@ def chat( stop: Union[List[str], str] | NotGiven = NOT_GIVEN, stream: Literal[False] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, + tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, + tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -66,7 +68,7 @@ def chat( the model to respond to. The array must contain at least one message. model: Specifies the model to be used for generating responses. The chat model is - always `palmyra-x-002-32k` for conversational use. + always `palmyra-x-004` for conversational use. max_tokens: Defines the maximum number of tokens (words and characters) that the model can generate in the response. The default value is set to 16, but it can be adjusted @@ -88,6 +90,13 @@ def chat( temperature results in more varied and less predictable text, while a lower temperature produces more deterministic and conservative outputs. + tool_choice: Configure how the model will call functions: `auto` will allow the model to + automatically choose the best tool, `none` disables tool calling. You can also + pass a specific previously defined function as a string. + + tools: An array of tools described to the model using JSON schema that the model can + use to generate responses. + top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with cumulative probability above this threshold are considered, controlling the @@ -114,6 +123,8 @@ def chat( n: int | NotGiven = NOT_GIVEN, stop: Union[List[str], str] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, + tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, + tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -130,7 +141,7 @@ def chat( the model to respond to. The array must contain at least one message. model: Specifies the model to be used for generating responses. The chat model is - always `palmyra-x-002-32k` for conversational use. + always `palmyra-x-004` for conversational use. stream: Indicates whether the response should be streamed incrementally as it is generated or only returned once fully complete. Streaming can be useful for @@ -152,6 +163,13 @@ def chat( temperature results in more varied and less predictable text, while a lower temperature produces more deterministic and conservative outputs. + tool_choice: Configure how the model will call functions: `auto` will allow the model to + automatically choose the best tool, `none` disables tool calling. You can also + pass a specific previously defined function as a string. + + tools: An array of tools described to the model using JSON schema that the model can + use to generate responses. + top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with cumulative probability above this threshold are considered, controlling the @@ -178,6 +196,8 @@ def chat( n: int | NotGiven = NOT_GIVEN, stop: Union[List[str], str] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, + tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, + tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -194,7 +214,7 @@ def chat( the model to respond to. The array must contain at least one message. model: Specifies the model to be used for generating responses. The chat model is - always `palmyra-x-002-32k` for conversational use. + always `palmyra-x-004` for conversational use. stream: Indicates whether the response should be streamed incrementally as it is generated or only returned once fully complete. Streaming can be useful for @@ -216,6 +236,13 @@ def chat( temperature results in more varied and less predictable text, while a lower temperature produces more deterministic and conservative outputs. + tool_choice: Configure how the model will call functions: `auto` will allow the model to + automatically choose the best tool, `none` disables tool calling. You can also + pass a specific previously defined function as a string. + + tools: An array of tools described to the model using JSON schema that the model can + use to generate responses. + top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with cumulative probability above this threshold are considered, controlling the @@ -242,6 +269,8 @@ def chat( stop: Union[List[str], str] | NotGiven = NOT_GIVEN, stream: Literal[False] | Literal[True] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, + tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, + tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -261,6 +290,8 @@ def chat( "stop": stop, "stream": stream, "temperature": temperature, + "tool_choice": tool_choice, + "tools": tools, "top_p": top_p, }, chat_chat_params.ChatChatParams, @@ -294,6 +325,8 @@ async def chat( stop: Union[List[str], str] | NotGiven = NOT_GIVEN, stream: Literal[False] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, + tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, + tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -310,7 +343,7 @@ async def chat( the model to respond to. The array must contain at least one message. model: Specifies the model to be used for generating responses. The chat model is - always `palmyra-x-002-32k` for conversational use. + always `palmyra-x-004` for conversational use. max_tokens: Defines the maximum number of tokens (words and characters) that the model can generate in the response. The default value is set to 16, but it can be adjusted @@ -332,6 +365,13 @@ async def chat( temperature results in more varied and less predictable text, while a lower temperature produces more deterministic and conservative outputs. + tool_choice: Configure how the model will call functions: `auto` will allow the model to + automatically choose the best tool, `none` disables tool calling. You can also + pass a specific previously defined function as a string. + + tools: An array of tools described to the model using JSON schema that the model can + use to generate responses. + top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with cumulative probability above this threshold are considered, controlling the @@ -358,6 +398,8 @@ async def chat( n: int | NotGiven = NOT_GIVEN, stop: Union[List[str], str] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, + tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, + tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -374,7 +416,7 @@ async def chat( the model to respond to. The array must contain at least one message. model: Specifies the model to be used for generating responses. The chat model is - always `palmyra-x-002-32k` for conversational use. + always `palmyra-x-004` for conversational use. stream: Indicates whether the response should be streamed incrementally as it is generated or only returned once fully complete. Streaming can be useful for @@ -396,6 +438,13 @@ async def chat( temperature results in more varied and less predictable text, while a lower temperature produces more deterministic and conservative outputs. + tool_choice: Configure how the model will call functions: `auto` will allow the model to + automatically choose the best tool, `none` disables tool calling. You can also + pass a specific previously defined function as a string. + + tools: An array of tools described to the model using JSON schema that the model can + use to generate responses. + top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with cumulative probability above this threshold are considered, controlling the @@ -422,6 +471,8 @@ async def chat( n: int | NotGiven = NOT_GIVEN, stop: Union[List[str], str] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, + tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, + tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -438,7 +489,7 @@ async def chat( the model to respond to. The array must contain at least one message. model: Specifies the model to be used for generating responses. The chat model is - always `palmyra-x-002-32k` for conversational use. + always `palmyra-x-004` for conversational use. stream: Indicates whether the response should be streamed incrementally as it is generated or only returned once fully complete. Streaming can be useful for @@ -460,6 +511,13 @@ async def chat( temperature results in more varied and less predictable text, while a lower temperature produces more deterministic and conservative outputs. + tool_choice: Configure how the model will call functions: `auto` will allow the model to + automatically choose the best tool, `none` disables tool calling. You can also + pass a specific previously defined function as a string. + + tools: An array of tools described to the model using JSON schema that the model can + use to generate responses. + top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with cumulative probability above this threshold are considered, controlling the @@ -486,6 +544,8 @@ async def chat( stop: Union[List[str], str] | NotGiven = NOT_GIVEN, stream: Literal[False] | Literal[True] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, + tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, + tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -505,6 +565,8 @@ async def chat( "stop": stop, "stream": stream, "temperature": temperature, + "tool_choice": tool_choice, + "tools": tools, "top_p": top_p, }, chat_chat_params.ChatChatParams, diff --git a/src/writerai/resources/files.py b/src/writerai/resources/files.py index a8b13cff..6890dfd5 100644 --- a/src/writerai/resources/files.py +++ b/src/writerai/resources/files.py @@ -86,6 +86,7 @@ def list( graph_id: str | NotGiven = NOT_GIVEN, limit: int | NotGiven = NOT_GIVEN, order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, + status: Literal["in_progress", "completed", "failed"] | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -112,6 +113,9 @@ def list( order: Specifies the order of the results. Valid values are asc for ascending and desc for descending. + status: Specifies the status of the files to retrieve. Valid values are in_progress, + completed or failed. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -135,6 +139,7 @@ def list( "graph_id": graph_id, "limit": limit, "order": order, + "status": status, }, file_list_params.FileListParams, ), @@ -294,6 +299,7 @@ def list( graph_id: str | NotGiven = NOT_GIVEN, limit: int | NotGiven = NOT_GIVEN, order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, + status: Literal["in_progress", "completed", "failed"] | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -320,6 +326,9 @@ def list( order: Specifies the order of the results. Valid values are asc for ascending and desc for descending. + status: Specifies the status of the files to retrieve. Valid values are in_progress, + completed or failed. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -343,6 +352,7 @@ def list( "graph_id": graph_id, "limit": limit, "order": order, + "status": status, }, file_list_params.FileListParams, ), diff --git a/src/writerai/types/chat.py b/src/writerai/types/chat.py index 3ff233f8..834f1d60 100644 --- a/src/writerai/types/chat.py +++ b/src/writerai/types/chat.py @@ -1,11 +1,27 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List +from typing import List, Optional from typing_extensions import Literal from .._models import BaseModel -__all__ = ["Chat", "Choice", "ChoiceMessage"] +__all__ = ["Chat", "Choice", "ChoiceMessage", "ChoiceMessageToolCall", "ChoiceMessageToolCallFunction"] + + +class ChoiceMessageToolCallFunction(BaseModel): + arguments: Optional[str] = None + + name: Optional[str] = None + + +class ChoiceMessageToolCall(BaseModel): + id: Optional[str] = None + + function: Optional[ChoiceMessageToolCallFunction] = None + + index: Optional[int] = None + + type: Optional[str] = None class ChoiceMessage(BaseModel): @@ -23,9 +39,11 @@ class ChoiceMessage(BaseModel): output within the interaction flow. """ + tool_calls: Optional[List[ChoiceMessageToolCall]] = None + class Choice(BaseModel): - finish_reason: Literal["stop", "length", "content_filter"] + finish_reason: Literal["stop", "length", "content_filter", "tool_calls"] """Describes the condition under which the model ceased generating content. Common reasons include 'length' (reached the maximum output size), 'stop' diff --git a/src/writerai/types/chat_chat_params.py b/src/writerai/types/chat_chat_params.py index f257ed8e..113ed71a 100644 --- a/src/writerai/types/chat_chat_params.py +++ b/src/writerai/types/chat_chat_params.py @@ -3,9 +3,19 @@ from __future__ import annotations from typing import List, Union, Iterable -from typing_extensions import Literal, Required, TypedDict +from typing_extensions import Literal, Required, TypeAlias, TypedDict -__all__ = ["ChatChatParamsBase", "Message", "ChatChatParamsNonStreaming", "ChatChatParamsStreaming"] +__all__ = [ + "ChatChatParamsBase", + "Message", + "ToolChoice", + "ToolChoiceJsonObjectToolChoice", + "ToolChoiceStringToolChoice", + "Tool", + "ToolFunction", + "ChatChatParamsNonStreaming", + "ChatChatParamsStreaming", +] class ChatChatParamsBase(TypedDict, total=False): @@ -18,7 +28,7 @@ class ChatChatParamsBase(TypedDict, total=False): model: Required[str] """Specifies the model to be used for generating responses. - The chat model is always `palmyra-x-002-32k` for conversational use. + The chat model is always `palmyra-x-004` for conversational use. """ max_tokens: int @@ -49,6 +59,19 @@ class ChatChatParamsBase(TypedDict, total=False): lower temperature produces more deterministic and conservative outputs. """ + tool_choice: ToolChoice + """ + Configure how the model will call functions: `auto` will allow the model to + automatically choose the best tool, `none` disables tool calling. You can also + pass a specific previously defined function as a string. + """ + + tools: Iterable[Tool] + """ + An array of tools described to the model using JSON schema that the model can + use to generate responses. + """ + top_p: float """ Sets the threshold for "nucleus sampling," a technique to focus the model's @@ -66,6 +89,31 @@ class Message(TypedDict, total=False): name: str +class ToolChoiceJsonObjectToolChoice(TypedDict, total=False): + value: Required[object] + + +class ToolChoiceStringToolChoice(TypedDict, total=False): + value: Required[Literal["none", "auto", "required"]] + + +ToolChoice: TypeAlias = Union[ToolChoiceJsonObjectToolChoice, ToolChoiceStringToolChoice] + + +class ToolFunction(TypedDict, total=False): + name: Required[str] + + description: str + + parameters: object + + +class Tool(TypedDict, total=False): + function: Required[ToolFunction] + + type: Required[str] + + class ChatChatParamsNonStreaming(ChatChatParamsBase): stream: Literal[False] """ diff --git a/src/writerai/types/file.py b/src/writerai/types/file.py index 8b23f13b..7d708f50 100644 --- a/src/writerai/types/file.py +++ b/src/writerai/types/file.py @@ -20,3 +20,6 @@ class File(BaseModel): name: str """The name of the file.""" + + status: str + """The processing status of the file.""" diff --git a/src/writerai/types/file_list_params.py b/src/writerai/types/file_list_params.py index 1a803015..00ad5b97 100644 --- a/src/writerai/types/file_list_params.py +++ b/src/writerai/types/file_list_params.py @@ -34,3 +34,9 @@ class FileListParams(TypedDict, total=False): Valid values are asc for ascending and desc for descending. """ + + status: Literal["in_progress", "completed", "failed"] + """Specifies the status of the files to retrieve. + + Valid values are in_progress, completed or failed. + """ diff --git a/tests/api_resources/test_chat.py b/tests/api_resources/test_chat.py index 1d74a3b3..f814782a 100644 --- a/tests/api_resources/test_chat.py +++ b/tests/api_resources/test_chat.py @@ -22,11 +22,11 @@ def test_method_chat_overload_1(self, client: Writer) -> None: chat = client.chat.chat( messages=[ { - "content": "content", + "content": "Write a memo summarizing this earnings report.", "role": "user", } ], - model="model", + model="palmyra-x-004", ) assert_matches_type(Chat, chat, path=["response"]) @@ -35,17 +35,44 @@ def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: chat = client.chat.chat( messages=[ { - "content": "content", + "content": "Write a memo summarizing this earnings report.", "role": "user", "name": "name", } ], - model="model", + model="palmyra-x-004", max_tokens=0, n=0, stop=["string", "string", "string"], stream=False, temperature=0, + tool_choice={"value": {}}, + tools=[ + { + "function": { + "name": "name", + "description": "description", + "parameters": {}, + }, + "type": "type", + }, + { + "function": { + "name": "name", + "description": "description", + "parameters": {}, + }, + "type": "type", + }, + { + "function": { + "name": "name", + "description": "description", + "parameters": {}, + }, + "type": "type", + }, + ], top_p=0, ) assert_matches_type(Chat, chat, path=["response"]) @@ -55,11 +82,11 @@ def test_raw_response_chat_overload_1(self, client: Writer) -> None: response = client.chat.with_raw_response.chat( messages=[ { - "content": "content", + "content": "Write a memo summarizing this earnings report.", "role": "user", } ], - model="model", + model="palmyra-x-004", ) assert response.is_closed is True @@ -72,11 +99,11 @@ def test_streaming_response_chat_overload_1(self, client: Writer) -> None: with client.chat.with_streaming_response.chat( messages=[ { - "content": "content", + "content": "Write a memo summarizing this earnings report.", "role": "user", } ], - model="model", + model="palmyra-x-004", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -91,11 +118,11 @@ def test_method_chat_overload_2(self, client: Writer) -> None: chat_stream = client.chat.chat( messages=[ { - "content": "content", + "content": "Write a memo summarizing this earnings report.", "role": "user", } ], - model="model", + model="palmyra-x-004", stream=True, ) chat_stream.response.close() @@ -105,17 +132,44 @@ def test_method_chat_with_all_params_overload_2(self, client: Writer) -> None: chat_stream = client.chat.chat( messages=[ { - "content": "content", + "content": "Write a memo summarizing this earnings report.", "role": "user", "name": "name", } ], - model="model", + model="palmyra-x-004", stream=True, max_tokens=0, n=0, stop=["string", "string", "string"], temperature=0, + tool_choice={"value": {}}, + tools=[ + { + "function": { + "name": "name", + "description": "description", + "parameters": {}, + }, + "type": "type", + }, + { + "function": { + "name": "name", + "description": "description", + "parameters": {}, + }, + "type": "type", + }, + { + "function": { + "name": "name", + "description": "description", + "parameters": {}, + }, + "type": "type", + }, + ], top_p=0, ) chat_stream.response.close() @@ -125,11 +179,11 @@ def test_raw_response_chat_overload_2(self, client: Writer) -> None: response = client.chat.with_raw_response.chat( messages=[ { - "content": "content", + "content": "Write a memo summarizing this earnings report.", "role": "user", } ], - model="model", + model="palmyra-x-004", stream=True, ) @@ -142,11 +196,11 @@ def test_streaming_response_chat_overload_2(self, client: Writer) -> None: with client.chat.with_streaming_response.chat( messages=[ { - "content": "content", + "content": "Write a memo summarizing this earnings report.", "role": "user", } ], - model="model", + model="palmyra-x-004", stream=True, ) as response: assert not response.is_closed @@ -166,11 +220,11 @@ async def test_method_chat_overload_1(self, async_client: AsyncWriter) -> None: chat = await async_client.chat.chat( messages=[ { - "content": "content", + "content": "Write a memo summarizing this earnings report.", "role": "user", } ], - model="model", + model="palmyra-x-004", ) assert_matches_type(Chat, chat, path=["response"]) @@ -179,17 +233,44 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW chat = await async_client.chat.chat( messages=[ { - "content": "content", + "content": "Write a memo summarizing this earnings report.", "role": "user", "name": "name", } ], - model="model", + model="palmyra-x-004", max_tokens=0, n=0, stop=["string", "string", "string"], stream=False, temperature=0, + tool_choice={"value": {}}, + tools=[ + { + "function": { + "name": "name", + "description": "description", + "parameters": {}, + }, + "type": "type", + }, + { + "function": { + "name": "name", + "description": "description", + "parameters": {}, + }, + "type": "type", + }, + { + "function": { + "name": "name", + "description": "description", + "parameters": {}, + }, + "type": "type", + }, + ], top_p=0, ) assert_matches_type(Chat, chat, path=["response"]) @@ -199,11 +280,11 @@ async def test_raw_response_chat_overload_1(self, async_client: AsyncWriter) -> response = await async_client.chat.with_raw_response.chat( messages=[ { - "content": "content", + "content": "Write a memo summarizing this earnings report.", "role": "user", } ], - model="model", + model="palmyra-x-004", ) assert response.is_closed is True @@ -216,11 +297,11 @@ async def test_streaming_response_chat_overload_1(self, async_client: AsyncWrite async with async_client.chat.with_streaming_response.chat( messages=[ { - "content": "content", + "content": "Write a memo summarizing this earnings report.", "role": "user", } ], - model="model", + model="palmyra-x-004", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -235,11 +316,11 @@ async def test_method_chat_overload_2(self, async_client: AsyncWriter) -> None: chat_stream = await async_client.chat.chat( messages=[ { - "content": "content", + "content": "Write a memo summarizing this earnings report.", "role": "user", } ], - model="model", + model="palmyra-x-004", stream=True, ) await chat_stream.response.aclose() @@ -249,17 +330,44 @@ async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncW chat_stream = await async_client.chat.chat( messages=[ { - "content": "content", + "content": "Write a memo summarizing this earnings report.", "role": "user", "name": "name", } ], - model="model", + model="palmyra-x-004", stream=True, max_tokens=0, n=0, stop=["string", "string", "string"], temperature=0, + tool_choice={"value": {}}, + tools=[ + { + "function": { + "name": "name", + "description": "description", + "parameters": {}, + }, + "type": "type", + }, + { + "function": { + "name": "name", + "description": "description", + "parameters": {}, + }, + "type": "type", + }, + { + "function": { + "name": "name", + "description": "description", + "parameters": {}, + }, + "type": "type", + }, + ], top_p=0, ) await chat_stream.response.aclose() @@ -269,11 +377,11 @@ async def test_raw_response_chat_overload_2(self, async_client: AsyncWriter) -> response = await async_client.chat.with_raw_response.chat( messages=[ { - "content": "content", + "content": "Write a memo summarizing this earnings report.", "role": "user", } ], - model="model", + model="palmyra-x-004", stream=True, ) @@ -286,11 +394,11 @@ async def test_streaming_response_chat_overload_2(self, async_client: AsyncWrite async with async_client.chat.with_streaming_response.chat( messages=[ { - "content": "content", + "content": "Write a memo summarizing this earnings report.", "role": "user", } ], - model="model", + model="palmyra-x-004", stream=True, ) as response: assert not response.is_closed diff --git a/tests/api_resources/test_completions.py b/tests/api_resources/test_completions.py index 4db055fa..e39a6d99 100644 --- a/tests/api_resources/test_completions.py +++ b/tests/api_resources/test_completions.py @@ -20,7 +20,7 @@ class TestCompletions: @parametrize def test_method_create_overload_1(self, client: Writer) -> None: completion = client.completions.create( - model="palmyra-x-002-instruct", + model="palmyra-x-003-instruct", prompt="Write me an SEO article about...", ) assert_matches_type(Completion, completion, path=["response"]) @@ -28,7 +28,7 @@ def test_method_create_overload_1(self, client: Writer) -> None: @parametrize def test_method_create_with_all_params_overload_1(self, client: Writer) -> None: completion = client.completions.create( - model="palmyra-x-002-instruct", + model="palmyra-x-003-instruct", prompt="Write me an SEO article about...", best_of=1, max_tokens=150, @@ -43,7 +43,7 @@ def test_method_create_with_all_params_overload_1(self, client: Writer) -> None: @parametrize def test_raw_response_create_overload_1(self, client: Writer) -> None: response = client.completions.with_raw_response.create( - model="palmyra-x-002-instruct", + model="palmyra-x-003-instruct", prompt="Write me an SEO article about...", ) @@ -55,7 +55,7 @@ def test_raw_response_create_overload_1(self, client: Writer) -> None: @parametrize def test_streaming_response_create_overload_1(self, client: Writer) -> None: with client.completions.with_streaming_response.create( - model="palmyra-x-002-instruct", + model="palmyra-x-003-instruct", prompt="Write me an SEO article about...", ) as response: assert not response.is_closed @@ -69,7 +69,7 @@ def test_streaming_response_create_overload_1(self, client: Writer) -> None: @parametrize def test_method_create_overload_2(self, client: Writer) -> None: completion_stream = client.completions.create( - model="palmyra-x-002-instruct", + model="palmyra-x-003-instruct", prompt="Write me an SEO article about...", stream=True, ) @@ -78,7 +78,7 @@ def test_method_create_overload_2(self, client: Writer) -> None: @parametrize def test_method_create_with_all_params_overload_2(self, client: Writer) -> None: completion_stream = client.completions.create( - model="palmyra-x-002-instruct", + model="palmyra-x-003-instruct", prompt="Write me an SEO article about...", stream=True, best_of=1, @@ -93,7 +93,7 @@ def test_method_create_with_all_params_overload_2(self, client: Writer) -> None: @parametrize def test_raw_response_create_overload_2(self, client: Writer) -> None: response = client.completions.with_raw_response.create( - model="palmyra-x-002-instruct", + model="palmyra-x-003-instruct", prompt="Write me an SEO article about...", stream=True, ) @@ -105,7 +105,7 @@ def test_raw_response_create_overload_2(self, client: Writer) -> None: @parametrize def test_streaming_response_create_overload_2(self, client: Writer) -> None: with client.completions.with_streaming_response.create( - model="palmyra-x-002-instruct", + model="palmyra-x-003-instruct", prompt="Write me an SEO article about...", stream=True, ) as response: @@ -124,7 +124,7 @@ class TestAsyncCompletions: @parametrize async def test_method_create_overload_1(self, async_client: AsyncWriter) -> None: completion = await async_client.completions.create( - model="palmyra-x-002-instruct", + model="palmyra-x-003-instruct", prompt="Write me an SEO article about...", ) assert_matches_type(Completion, completion, path=["response"]) @@ -132,7 +132,7 @@ async def test_method_create_overload_1(self, async_client: AsyncWriter) -> None @parametrize async def test_method_create_with_all_params_overload_1(self, async_client: AsyncWriter) -> None: completion = await async_client.completions.create( - model="palmyra-x-002-instruct", + model="palmyra-x-003-instruct", prompt="Write me an SEO article about...", best_of=1, max_tokens=150, @@ -147,7 +147,7 @@ async def test_method_create_with_all_params_overload_1(self, async_client: Asyn @parametrize async def test_raw_response_create_overload_1(self, async_client: AsyncWriter) -> None: response = await async_client.completions.with_raw_response.create( - model="palmyra-x-002-instruct", + model="palmyra-x-003-instruct", prompt="Write me an SEO article about...", ) @@ -159,7 +159,7 @@ async def test_raw_response_create_overload_1(self, async_client: AsyncWriter) - @parametrize async def test_streaming_response_create_overload_1(self, async_client: AsyncWriter) -> None: async with async_client.completions.with_streaming_response.create( - model="palmyra-x-002-instruct", + model="palmyra-x-003-instruct", prompt="Write me an SEO article about...", ) as response: assert not response.is_closed @@ -173,7 +173,7 @@ async def test_streaming_response_create_overload_1(self, async_client: AsyncWri @parametrize async def test_method_create_overload_2(self, async_client: AsyncWriter) -> None: completion_stream = await async_client.completions.create( - model="palmyra-x-002-instruct", + model="palmyra-x-003-instruct", prompt="Write me an SEO article about...", stream=True, ) @@ -182,7 +182,7 @@ async def test_method_create_overload_2(self, async_client: AsyncWriter) -> None @parametrize async def test_method_create_with_all_params_overload_2(self, async_client: AsyncWriter) -> None: completion_stream = await async_client.completions.create( - model="palmyra-x-002-instruct", + model="palmyra-x-003-instruct", prompt="Write me an SEO article about...", stream=True, best_of=1, @@ -197,7 +197,7 @@ async def test_method_create_with_all_params_overload_2(self, async_client: Asyn @parametrize async def test_raw_response_create_overload_2(self, async_client: AsyncWriter) -> None: response = await async_client.completions.with_raw_response.create( - model="palmyra-x-002-instruct", + model="palmyra-x-003-instruct", prompt="Write me an SEO article about...", stream=True, ) @@ -209,7 +209,7 @@ async def test_raw_response_create_overload_2(self, async_client: AsyncWriter) - @parametrize async def test_streaming_response_create_overload_2(self, async_client: AsyncWriter) -> None: async with async_client.completions.with_streaming_response.create( - model="palmyra-x-002-instruct", + model="palmyra-x-003-instruct", prompt="Write me an SEO article about...", stream=True, ) as response: diff --git a/tests/api_resources/test_files.py b/tests/api_resources/test_files.py index 85041dcf..45cbb33f 100644 --- a/tests/api_resources/test_files.py +++ b/tests/api_resources/test_files.py @@ -77,6 +77,7 @@ def test_method_list_with_all_params(self, client: Writer) -> None: graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", limit=0, order="asc", + status="in_progress", ) assert_matches_type(SyncCursorPage[File], file, path=["response"]) @@ -284,6 +285,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncWriter) -> N graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", limit=0, order="asc", + status="in_progress", ) assert_matches_type(AsyncCursorPage[File], file, path=["response"]) diff --git a/tests/test_client.py b/tests/test_client.py index 4bf7e0df..bc35c7cc 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -726,7 +726,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> No dict( messages=[ { - "content": "content", + "content": "Write a memo summarizing this earnings report.", "role": "user", } ], @@ -752,7 +752,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> Non dict( messages=[ { - "content": "content", + "content": "Write a memo summarizing this earnings report.", "role": "user", } ], @@ -785,11 +785,11 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: response = client.chat.with_raw_response.chat( messages=[ { - "content": "content", + "content": "Write a memo summarizing this earnings report.", "role": "user", } ], - model="model", + model="palmyra-x-004", ) assert response.retries_taken == failures_before_success @@ -1487,7 +1487,7 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) dict( messages=[ { - "content": "content", + "content": "Write a memo summarizing this earnings report.", "role": "user", } ], @@ -1513,7 +1513,7 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) dict( messages=[ { - "content": "content", + "content": "Write a memo summarizing this earnings report.", "role": "user", } ], @@ -1549,11 +1549,11 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: response = await client.chat.with_raw_response.chat( messages=[ { - "content": "content", + "content": "Write a memo summarizing this earnings report.", "role": "user", } ], - model="model", + model="palmyra-x-004", ) assert response.retries_taken == failures_before_success From 868b38e9c45370b32e7152e24d36f2fd0d73c65b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Sep 2024 17:20:16 +0000 Subject: [PATCH 071/399] chore(internal): codegen related update (#65) --- CONTRIBUTING.md | 8 +++---- README.md | 11 ++++++++++ requirements-dev.lock | 6 ++--- src/writerai/_utils/_utils.py | 7 +++--- src/writerai/resources/applications.py | 22 +++++++++++++++++++ src/writerai/resources/chat.py | 22 +++++++++++++++++++ src/writerai/resources/completions.py | 22 +++++++++++++++++++ src/writerai/resources/files.py | 22 +++++++++++++++++++ src/writerai/resources/graphs.py | 22 +++++++++++++++++++ src/writerai/resources/models.py | 22 +++++++++++++++++++ src/writerai/types/chat_chat_params.py | 2 +- .../types/completion_create_params.py | 2 +- 12 files changed, 156 insertions(+), 12 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ac2a7dfa..4335d76c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,13 +31,13 @@ $ pip install -r requirements-dev.lock ## Modifying/Adding code -Most of the SDK is generated code, and any modified code will be overridden on the next generation. The -`src/writerai/lib/` and `examples/` directories are exceptions and will never be overridden. +Most of the SDK is generated code. Modifications to code will be persisted between generations, but may +result in merge conflicts between manual patches and changes from the generator. The generator will never +modify the contents of the `src/writerai/lib/` and `examples/` directories. ## Adding and running examples -All files in the `examples/` directory are not modified by the Stainless generator and can be freely edited or -added to. +All files in the `examples/` directory are not modified by the generator and can be freely edited or added to. ```bash # add an example to examples/.py diff --git a/README.md b/README.md index a933bdb0..fa8591f8 100644 --- a/README.md +++ b/README.md @@ -389,6 +389,17 @@ We take backwards-compatibility seriously and work hard to ensure you can rely o We are keen for your feedback; please open an [issue](https://www.github.com/writer/writer-python/issues) with questions, bugs, or suggestions. +### Determining the installed version + +If you've upgraded to the latest version but aren't seeing any new features you were expecting then your python environment is likely still using an older version. + +You can determine the version that is being used at runtime with: + +```py +import writerai +print(writerai.__version__) +``` + ## Requirements Python 3.7 or higher. diff --git a/requirements-dev.lock b/requirements-dev.lock index baa25f20..d3ee1e6d 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -49,7 +49,7 @@ markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -mypy==1.10.1 +mypy==1.11.2 mypy-extensions==1.0.0 # via mypy nodeenv==1.8.0 @@ -70,7 +70,7 @@ pydantic-core==2.18.2 # via pydantic pygments==2.18.0 # via rich -pyright==1.1.374 +pyright==1.1.380 pytest==7.1.1 # via pytest-asyncio pytest-asyncio==0.21.1 @@ -80,7 +80,7 @@ pytz==2023.3.post1 # via dirty-equals respx==0.20.2 rich==13.7.1 -ruff==0.5.6 +ruff==0.6.5 setuptools==68.2.2 # via nodeenv six==1.16.0 diff --git a/src/writerai/_utils/_utils.py b/src/writerai/_utils/_utils.py index 2fc5a1c6..0bba17ca 100644 --- a/src/writerai/_utils/_utils.py +++ b/src/writerai/_utils/_utils.py @@ -363,12 +363,13 @@ def file_from_path(path: str) -> FileTypes: def get_required_header(headers: HeadersLike, header: str) -> str: lower_header = header.lower() - if isinstance(headers, Mapping): - for k, v in headers.items(): + if is_mapping_t(headers): + # mypy doesn't understand the type narrowing here + for k, v in headers.items(): # type: ignore if k.lower() == lower_header and isinstance(v, str): return v - """ to deal with the case where the header looks like Stainless-Event-Id """ + # to deal with the case where the header looks like Stainless-Event-Id intercaps_header = re.sub(r"([^\w])(\w)", lambda pat: pat.group(1) + pat.group(2).upper(), header.capitalize()) for normalized_header in [header, lower_header, header.upper(), intercaps_header]: diff --git a/src/writerai/resources/applications.py b/src/writerai/resources/applications.py index e32d7404..a9f031fd 100644 --- a/src/writerai/resources/applications.py +++ b/src/writerai/resources/applications.py @@ -29,10 +29,21 @@ class ApplicationsResource(SyncAPIResource): @cached_property def with_raw_response(self) -> ApplicationsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers + """ return ApplicationsResourceWithRawResponse(self) @cached_property def with_streaming_response(self) -> ApplicationsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/writer/writer-python#with_streaming_response + """ return ApplicationsResourceWithStreamingResponse(self) def generate_content( @@ -76,10 +87,21 @@ def generate_content( class AsyncApplicationsResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncApplicationsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers + """ return AsyncApplicationsResourceWithRawResponse(self) @cached_property def with_streaming_response(self) -> AsyncApplicationsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/writer/writer-python#with_streaming_response + """ return AsyncApplicationsResourceWithStreamingResponse(self) async def generate_content( diff --git a/src/writerai/resources/chat.py b/src/writerai/resources/chat.py index 4d13ab81..3ccead31 100644 --- a/src/writerai/resources/chat.py +++ b/src/writerai/resources/chat.py @@ -33,10 +33,21 @@ class ChatResource(SyncAPIResource): @cached_property def with_raw_response(self) -> ChatResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers + """ return ChatResourceWithRawResponse(self) @cached_property def with_streaming_response(self) -> ChatResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/writer/writer-python#with_streaming_response + """ return ChatResourceWithStreamingResponse(self) @overload @@ -308,10 +319,21 @@ def chat( class AsyncChatResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncChatResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers + """ return AsyncChatResourceWithRawResponse(self) @cached_property def with_streaming_response(self) -> AsyncChatResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/writer/writer-python#with_streaming_response + """ return AsyncChatResourceWithStreamingResponse(self) @overload diff --git a/src/writerai/resources/completions.py b/src/writerai/resources/completions.py index 6962f7b1..addef7fc 100644 --- a/src/writerai/resources/completions.py +++ b/src/writerai/resources/completions.py @@ -33,10 +33,21 @@ class CompletionsResource(SyncAPIResource): @cached_property def with_raw_response(self) -> CompletionsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers + """ return CompletionsResourceWithRawResponse(self) @cached_property def with_streaming_response(self) -> CompletionsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/writer/writer-python#with_streaming_response + """ return CompletionsResourceWithStreamingResponse(self) @overload @@ -270,10 +281,21 @@ def create( class AsyncCompletionsResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncCompletionsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers + """ return AsyncCompletionsResourceWithRawResponse(self) @cached_property def with_streaming_response(self) -> AsyncCompletionsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/writer/writer-python#with_streaming_response + """ return AsyncCompletionsResourceWithStreamingResponse(self) @overload diff --git a/src/writerai/resources/files.py b/src/writerai/resources/files.py index 6890dfd5..fc8ebe28 100644 --- a/src/writerai/resources/files.py +++ b/src/writerai/resources/files.py @@ -39,10 +39,21 @@ class FilesResource(SyncAPIResource): @cached_property def with_raw_response(self) -> FilesResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers + """ return FilesResourceWithRawResponse(self) @cached_property def with_streaming_response(self) -> FilesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/writer/writer-python#with_streaming_response + """ return FilesResourceWithStreamingResponse(self) def retrieve( @@ -252,10 +263,21 @@ def upload( class AsyncFilesResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncFilesResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers + """ return AsyncFilesResourceWithRawResponse(self) @cached_property def with_streaming_response(self) -> AsyncFilesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/writer/writer-python#with_streaming_response + """ return AsyncFilesResourceWithStreamingResponse(self) async def retrieve( diff --git a/src/writerai/resources/graphs.py b/src/writerai/resources/graphs.py index e157ad8d..76c84071 100644 --- a/src/writerai/resources/graphs.py +++ b/src/writerai/resources/graphs.py @@ -40,10 +40,21 @@ class GraphsResource(SyncAPIResource): @cached_property def with_raw_response(self) -> GraphsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers + """ return GraphsResourceWithRawResponse(self) @cached_property def with_streaming_response(self) -> GraphsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/writer/writer-python#with_streaming_response + """ return GraphsResourceWithStreamingResponse(self) def create( @@ -340,10 +351,21 @@ def remove_file_from_graph( class AsyncGraphsResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncGraphsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers + """ return AsyncGraphsResourceWithRawResponse(self) @cached_property def with_streaming_response(self) -> AsyncGraphsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/writer/writer-python#with_streaming_response + """ return AsyncGraphsResourceWithStreamingResponse(self) async def create( diff --git a/src/writerai/resources/models.py b/src/writerai/resources/models.py index 14028e91..68410156 100644 --- a/src/writerai/resources/models.py +++ b/src/writerai/resources/models.py @@ -22,10 +22,21 @@ class ModelsResource(SyncAPIResource): @cached_property def with_raw_response(self) -> ModelsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers + """ return ModelsResourceWithRawResponse(self) @cached_property def with_streaming_response(self) -> ModelsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/writer/writer-python#with_streaming_response + """ return ModelsResourceWithStreamingResponse(self) def list( @@ -51,10 +62,21 @@ def list( class AsyncModelsResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncModelsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers + """ return AsyncModelsResourceWithRawResponse(self) @cached_property def with_streaming_response(self) -> AsyncModelsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/writer/writer-python#with_streaming_response + """ return AsyncModelsResourceWithStreamingResponse(self) async def list( diff --git a/src/writerai/types/chat_chat_params.py b/src/writerai/types/chat_chat_params.py index 113ed71a..96d27aa6 100644 --- a/src/writerai/types/chat_chat_params.py +++ b/src/writerai/types/chat_chat_params.py @@ -114,7 +114,7 @@ class Tool(TypedDict, total=False): type: Required[str] -class ChatChatParamsNonStreaming(ChatChatParamsBase): +class ChatChatParamsNonStreaming(ChatChatParamsBase, total=False): stream: Literal[False] """ Indicates whether the response should be streamed incrementally as it is diff --git a/src/writerai/types/completion_create_params.py b/src/writerai/types/completion_create_params.py index 6436f60d..20ad406d 100644 --- a/src/writerai/types/completion_create_params.py +++ b/src/writerai/types/completion_create_params.py @@ -53,7 +53,7 @@ class CompletionCreateParamsBase(TypedDict, total=False): """ -class CompletionCreateParamsNonStreaming(CompletionCreateParamsBase): +class CompletionCreateParamsNonStreaming(CompletionCreateParamsBase, total=False): stream: Literal[False] """Determines whether the model's output should be streamed. From 162b3671927739018f074278cbd5ca7d5ade6ee2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Sep 2024 17:25:00 +0000 Subject: [PATCH 072/399] fix(client): handle domains with underscores (#67) --- src/writerai/_base_client.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index 35ef4b62..e04c5323 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -489,12 +489,17 @@ def _build_request( if not files: files = cast(HttpxRequestFiles, ForceMultipartDict()) + prepared_url = self._prepare_url(options.url) + if "_" in prepared_url.host: + # work around https://github.com/encode/httpx/discussions/2880 + kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")} + # TODO: report this error to httpx return self._client.build_request( # pyright: ignore[reportUnknownMemberType] headers=headers, timeout=self.timeout if isinstance(options.timeout, NotGiven) else options.timeout, method=options.method, - url=self._prepare_url(options.url), + url=prepared_url, # the `Query` type that we use is incompatible with qs' # `Params` type as it needs to be typed as `Mapping[str, object]` # so that passing a `TypedDict` doesn't cause an error. From 33b208d2cb285ebfbb9c1ac2d3a377bc3ebf6a3a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Sep 2024 17:25:49 +0000 Subject: [PATCH 073/399] feat(client): send retry count header (#68) --- src/writerai/_base_client.py | 101 +++++++++++++++++++---------------- tests/test_client.py | 2 + 2 files changed, 56 insertions(+), 47 deletions(-) diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index e04c5323..762a7c41 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -400,14 +400,7 @@ def _make_status_error( ) -> _exceptions.APIStatusError: raise NotImplementedError() - def _remaining_retries( - self, - remaining_retries: Optional[int], - options: FinalRequestOptions, - ) -> int: - return remaining_retries if remaining_retries is not None else options.get_max_retries(self.max_retries) - - def _build_headers(self, options: FinalRequestOptions) -> httpx.Headers: + def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0) -> httpx.Headers: custom_headers = options.headers or {} headers_dict = _merge_mappings(self.default_headers, custom_headers) self._validate_headers(headers_dict, custom_headers) @@ -419,6 +412,8 @@ def _build_headers(self, options: FinalRequestOptions) -> httpx.Headers: if idempotency_header and options.method.lower() != "get" and idempotency_header not in headers: headers[idempotency_header] = options.idempotency_key or self._idempotency_key() + headers.setdefault("x-stainless-retry-count", str(retries_taken)) + return headers def _prepare_url(self, url: str) -> URL: @@ -440,6 +435,8 @@ def _make_sse_decoder(self) -> SSEDecoder | SSEBytesDecoder: def _build_request( self, options: FinalRequestOptions, + *, + retries_taken: int = 0, ) -> httpx.Request: if log.isEnabledFor(logging.DEBUG): log.debug("Request options: %s", model_dump(options, exclude_unset=True)) @@ -455,7 +452,7 @@ def _build_request( else: raise RuntimeError(f"Unexpected JSON data type, {type(json_data)}, cannot merge with `extra_body`") - headers = self._build_headers(options) + headers = self._build_headers(options, retries_taken=retries_taken) params = _merge_mappings(self.default_query, options.params) content_type = headers.get("Content-Type") files = options.files @@ -938,12 +935,17 @@ def request( stream: bool = False, stream_cls: type[_StreamT] | None = None, ) -> ResponseT | _StreamT: + if remaining_retries is not None: + retries_taken = options.get_max_retries(self.max_retries) - remaining_retries + else: + retries_taken = 0 + return self._request( cast_to=cast_to, options=options, stream=stream, stream_cls=stream_cls, - remaining_retries=remaining_retries, + retries_taken=retries_taken, ) def _request( @@ -951,7 +953,7 @@ def _request( *, cast_to: Type[ResponseT], options: FinalRequestOptions, - remaining_retries: int | None, + retries_taken: int, stream: bool, stream_cls: type[_StreamT] | None, ) -> ResponseT | _StreamT: @@ -963,8 +965,8 @@ def _request( cast_to = self._maybe_override_cast_to(cast_to, options) options = self._prepare_options(options) - retries = self._remaining_retries(remaining_retries, options) - request = self._build_request(options) + remaining_retries = options.get_max_retries(self.max_retries) - retries_taken + request = self._build_request(options, retries_taken=retries_taken) self._prepare_request(request) kwargs: HttpxSendArgs = {} @@ -982,11 +984,11 @@ def _request( except httpx.TimeoutException as err: log.debug("Encountered httpx.TimeoutException", exc_info=True) - if retries > 0: + if remaining_retries > 0: return self._retry_request( input_options, cast_to, - retries, + retries_taken=retries_taken, stream=stream, stream_cls=stream_cls, response_headers=None, @@ -997,11 +999,11 @@ def _request( except Exception as err: log.debug("Encountered Exception", exc_info=True) - if retries > 0: + if remaining_retries > 0: return self._retry_request( input_options, cast_to, - retries, + retries_taken=retries_taken, stream=stream, stream_cls=stream_cls, response_headers=None, @@ -1024,13 +1026,13 @@ def _request( except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code log.debug("Encountered httpx.HTTPStatusError", exc_info=True) - if retries > 0 and self._should_retry(err.response): + if remaining_retries > 0 and self._should_retry(err.response): err.response.close() return self._retry_request( input_options, cast_to, - retries, - err.response.headers, + retries_taken=retries_taken, + response_headers=err.response.headers, stream=stream, stream_cls=stream_cls, ) @@ -1049,26 +1051,26 @@ def _request( response=response, stream=stream, stream_cls=stream_cls, - retries_taken=options.get_max_retries(self.max_retries) - retries, + retries_taken=retries_taken, ) def _retry_request( self, options: FinalRequestOptions, cast_to: Type[ResponseT], - remaining_retries: int, - response_headers: httpx.Headers | None, *, + retries_taken: int, + response_headers: httpx.Headers | None, stream: bool, stream_cls: type[_StreamT] | None, ) -> ResponseT | _StreamT: - remaining = remaining_retries - 1 - if remaining == 1: + remaining_retries = options.get_max_retries(self.max_retries) - retries_taken + if remaining_retries == 1: log.debug("1 retry left") else: - log.debug("%i retries left", remaining) + log.debug("%i retries left", remaining_retries) - timeout = self._calculate_retry_timeout(remaining, options, response_headers) + timeout = self._calculate_retry_timeout(remaining_retries, options, response_headers) log.info("Retrying request to %s in %f seconds", options.url, timeout) # In a synchronous context we are blocking the entire thread. Up to the library user to run the client in a @@ -1078,7 +1080,7 @@ def _retry_request( return self._request( options=options, cast_to=cast_to, - remaining_retries=remaining, + retries_taken=retries_taken + 1, stream=stream, stream_cls=stream_cls, ) @@ -1496,12 +1498,17 @@ async def request( stream_cls: type[_AsyncStreamT] | None = None, remaining_retries: Optional[int] = None, ) -> ResponseT | _AsyncStreamT: + if remaining_retries is not None: + retries_taken = options.get_max_retries(self.max_retries) - remaining_retries + else: + retries_taken = 0 + return await self._request( cast_to=cast_to, options=options, stream=stream, stream_cls=stream_cls, - remaining_retries=remaining_retries, + retries_taken=retries_taken, ) async def _request( @@ -1511,7 +1518,7 @@ async def _request( *, stream: bool, stream_cls: type[_AsyncStreamT] | None, - remaining_retries: int | None, + retries_taken: int, ) -> ResponseT | _AsyncStreamT: if self._platform is None: # `get_platform` can make blocking IO calls so we @@ -1526,8 +1533,8 @@ async def _request( cast_to = self._maybe_override_cast_to(cast_to, options) options = await self._prepare_options(options) - retries = self._remaining_retries(remaining_retries, options) - request = self._build_request(options) + remaining_retries = options.get_max_retries(self.max_retries) - retries_taken + request = self._build_request(options, retries_taken=retries_taken) await self._prepare_request(request) kwargs: HttpxSendArgs = {} @@ -1543,11 +1550,11 @@ async def _request( except httpx.TimeoutException as err: log.debug("Encountered httpx.TimeoutException", exc_info=True) - if retries > 0: + if remaining_retries > 0: return await self._retry_request( input_options, cast_to, - retries, + retries_taken=retries_taken, stream=stream, stream_cls=stream_cls, response_headers=None, @@ -1558,11 +1565,11 @@ async def _request( except Exception as err: log.debug("Encountered Exception", exc_info=True) - if retries > 0: + if retries_taken > 0: return await self._retry_request( input_options, cast_to, - retries, + retries_taken=retries_taken, stream=stream, stream_cls=stream_cls, response_headers=None, @@ -1580,13 +1587,13 @@ async def _request( except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code log.debug("Encountered httpx.HTTPStatusError", exc_info=True) - if retries > 0 and self._should_retry(err.response): + if remaining_retries > 0 and self._should_retry(err.response): await err.response.aclose() return await self._retry_request( input_options, cast_to, - retries, - err.response.headers, + retries_taken=retries_taken, + response_headers=err.response.headers, stream=stream, stream_cls=stream_cls, ) @@ -1605,26 +1612,26 @@ async def _request( response=response, stream=stream, stream_cls=stream_cls, - retries_taken=options.get_max_retries(self.max_retries) - retries, + retries_taken=retries_taken, ) async def _retry_request( self, options: FinalRequestOptions, cast_to: Type[ResponseT], - remaining_retries: int, - response_headers: httpx.Headers | None, *, + retries_taken: int, + response_headers: httpx.Headers | None, stream: bool, stream_cls: type[_AsyncStreamT] | None, ) -> ResponseT | _AsyncStreamT: - remaining = remaining_retries - 1 - if remaining == 1: + remaining_retries = options.get_max_retries(self.max_retries) - retries_taken + if remaining_retries == 1: log.debug("1 retry left") else: - log.debug("%i retries left", remaining) + log.debug("%i retries left", remaining_retries) - timeout = self._calculate_retry_timeout(remaining, options, response_headers) + timeout = self._calculate_retry_timeout(remaining_retries, options, response_headers) log.info("Retrying request to %s in %f seconds", options.url, timeout) await anyio.sleep(timeout) @@ -1632,7 +1639,7 @@ async def _retry_request( return await self._request( options=options, cast_to=cast_to, - remaining_retries=remaining, + retries_taken=retries_taken + 1, stream=stream, stream_cls=stream_cls, ) diff --git a/tests/test_client.py b/tests/test_client.py index bc35c7cc..86b32f06 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -793,6 +793,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: ) assert response.retries_taken == failures_before_success + assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success class TestAsyncWriter: @@ -1557,3 +1558,4 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: ) assert response.retries_taken == failures_before_success + assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success From 2770c0b4a30cff62a49e1d286ddd7217c7a37cba Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Sep 2024 15:24:17 +0000 Subject: [PATCH 074/399] feat(api): manual updates (#69) --- .stats.yml | 2 +- src/writerai/_compat.py | 2 ++ src/writerai/resources/chat.py | 16 ++++++++-------- src/writerai/resources/completions.py | 4 ++-- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.stats.yml b/.stats.yml index 5ca4b6e0..84abe5ca 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 16 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-149f74d46fcb6e37bb59c571100cf81230cf567855570f301cbb2d18d3a84764.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-a865ea2e74497617f3caa5cbf2301d6e7d6f86ee6b922fa40c5b6f80a477f290.yml diff --git a/src/writerai/_compat.py b/src/writerai/_compat.py index 21fe6941..162a6fbe 100644 --- a/src/writerai/_compat.py +++ b/src/writerai/_compat.py @@ -136,12 +136,14 @@ def model_dump( exclude: IncEx = None, exclude_unset: bool = False, exclude_defaults: bool = False, + warnings: bool = True, ) -> dict[str, Any]: if PYDANTIC_V2: return model.model_dump( exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, + warnings=warnings, ) return cast( "dict[str, Any]", diff --git a/src/writerai/resources/chat.py b/src/writerai/resources/chat.py index 3ccead31..bd2f9454 100644 --- a/src/writerai/resources/chat.py +++ b/src/writerai/resources/chat.py @@ -2,8 +2,8 @@ from __future__ import annotations -from typing import List, Union, Iterable, overload -from typing_extensions import Literal +from typing import List, Union, Iterable +from typing_extensions import Literal, overload import httpx @@ -72,7 +72,7 @@ def chat( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> Chat: """ - Chat completion + Chat completion v2 Args: messages: An array of message objects that form the conversation history or context for @@ -145,7 +145,7 @@ def chat( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> Stream[ChatStreamingData]: """ - Chat completion + Chat completion v2 Args: messages: An array of message objects that form the conversation history or context for @@ -218,7 +218,7 @@ def chat( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> Chat | Stream[ChatStreamingData]: """ - Chat completion + Chat completion v2 Args: messages: An array of message objects that form the conversation history or context for @@ -358,7 +358,7 @@ async def chat( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> Chat: """ - Chat completion + Chat completion v2 Args: messages: An array of message objects that form the conversation history or context for @@ -431,7 +431,7 @@ async def chat( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> AsyncStream[ChatStreamingData]: """ - Chat completion + Chat completion v2 Args: messages: An array of message objects that form the conversation history or context for @@ -504,7 +504,7 @@ async def chat( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> Chat | AsyncStream[ChatStreamingData]: """ - Chat completion + Chat completion v2 Args: messages: An array of message objects that form the conversation history or context for diff --git a/src/writerai/resources/completions.py b/src/writerai/resources/completions.py index addef7fc..81162da0 100644 --- a/src/writerai/resources/completions.py +++ b/src/writerai/resources/completions.py @@ -2,8 +2,8 @@ from __future__ import annotations -from typing import List, Union, overload -from typing_extensions import Literal +from typing import List, Union +from typing_extensions import Literal, overload import httpx From 2e3a57ab4daefa535c7373ede650d339f963e4a4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Sep 2024 15:55:15 +0000 Subject: [PATCH 075/399] docs(api): updates to API spec (#70) --- .stats.yml | 2 +- src/writerai/resources/chat.py | 74 ++--------------- src/writerai/types/chat.py | 24 +----- src/writerai/types/chat_chat_params.py | 52 +----------- tests/api_resources/test_chat.py | 108 ------------------------- 5 files changed, 12 insertions(+), 248 deletions(-) diff --git a/.stats.yml b/.stats.yml index 84abe5ca..a703388b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 16 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-a865ea2e74497617f3caa5cbf2301d6e7d6f86ee6b922fa40c5b6f80a477f290.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-5d34056e0e749d39a81c92ed4fb221da6ac627cff4cb9edcb6b054fa00abb335.yml diff --git a/src/writerai/resources/chat.py b/src/writerai/resources/chat.py index bd2f9454..8b24b15e 100644 --- a/src/writerai/resources/chat.py +++ b/src/writerai/resources/chat.py @@ -61,8 +61,6 @@ def chat( stop: Union[List[str], str] | NotGiven = NOT_GIVEN, stream: Literal[False] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, - tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -72,7 +70,7 @@ def chat( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> Chat: """ - Chat completion v2 + Chat completion Args: messages: An array of message objects that form the conversation history or context for @@ -101,13 +99,6 @@ def chat( temperature results in more varied and less predictable text, while a lower temperature produces more deterministic and conservative outputs. - tool_choice: Configure how the model will call functions: `auto` will allow the model to - automatically choose the best tool, `none` disables tool calling. You can also - pass a specific previously defined function as a string. - - tools: An array of tools described to the model using JSON schema that the model can - use to generate responses. - top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with cumulative probability above this threshold are considered, controlling the @@ -134,8 +125,6 @@ def chat( n: int | NotGiven = NOT_GIVEN, stop: Union[List[str], str] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, - tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -145,7 +134,7 @@ def chat( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> Stream[ChatStreamingData]: """ - Chat completion v2 + Chat completion Args: messages: An array of message objects that form the conversation history or context for @@ -174,13 +163,6 @@ def chat( temperature results in more varied and less predictable text, while a lower temperature produces more deterministic and conservative outputs. - tool_choice: Configure how the model will call functions: `auto` will allow the model to - automatically choose the best tool, `none` disables tool calling. You can also - pass a specific previously defined function as a string. - - tools: An array of tools described to the model using JSON schema that the model can - use to generate responses. - top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with cumulative probability above this threshold are considered, controlling the @@ -207,8 +189,6 @@ def chat( n: int | NotGiven = NOT_GIVEN, stop: Union[List[str], str] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, - tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -218,7 +198,7 @@ def chat( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> Chat | Stream[ChatStreamingData]: """ - Chat completion v2 + Chat completion Args: messages: An array of message objects that form the conversation history or context for @@ -247,13 +227,6 @@ def chat( temperature results in more varied and less predictable text, while a lower temperature produces more deterministic and conservative outputs. - tool_choice: Configure how the model will call functions: `auto` will allow the model to - automatically choose the best tool, `none` disables tool calling. You can also - pass a specific previously defined function as a string. - - tools: An array of tools described to the model using JSON schema that the model can - use to generate responses. - top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with cumulative probability above this threshold are considered, controlling the @@ -280,8 +253,6 @@ def chat( stop: Union[List[str], str] | NotGiven = NOT_GIVEN, stream: Literal[False] | Literal[True] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, - tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -301,8 +272,6 @@ def chat( "stop": stop, "stream": stream, "temperature": temperature, - "tool_choice": tool_choice, - "tools": tools, "top_p": top_p, }, chat_chat_params.ChatChatParams, @@ -347,8 +316,6 @@ async def chat( stop: Union[List[str], str] | NotGiven = NOT_GIVEN, stream: Literal[False] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, - tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -358,7 +325,7 @@ async def chat( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> Chat: """ - Chat completion v2 + Chat completion Args: messages: An array of message objects that form the conversation history or context for @@ -387,13 +354,6 @@ async def chat( temperature results in more varied and less predictable text, while a lower temperature produces more deterministic and conservative outputs. - tool_choice: Configure how the model will call functions: `auto` will allow the model to - automatically choose the best tool, `none` disables tool calling. You can also - pass a specific previously defined function as a string. - - tools: An array of tools described to the model using JSON schema that the model can - use to generate responses. - top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with cumulative probability above this threshold are considered, controlling the @@ -420,8 +380,6 @@ async def chat( n: int | NotGiven = NOT_GIVEN, stop: Union[List[str], str] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, - tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -431,7 +389,7 @@ async def chat( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> AsyncStream[ChatStreamingData]: """ - Chat completion v2 + Chat completion Args: messages: An array of message objects that form the conversation history or context for @@ -460,13 +418,6 @@ async def chat( temperature results in more varied and less predictable text, while a lower temperature produces more deterministic and conservative outputs. - tool_choice: Configure how the model will call functions: `auto` will allow the model to - automatically choose the best tool, `none` disables tool calling. You can also - pass a specific previously defined function as a string. - - tools: An array of tools described to the model using JSON schema that the model can - use to generate responses. - top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with cumulative probability above this threshold are considered, controlling the @@ -493,8 +444,6 @@ async def chat( n: int | NotGiven = NOT_GIVEN, stop: Union[List[str], str] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, - tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -504,7 +453,7 @@ async def chat( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> Chat | AsyncStream[ChatStreamingData]: """ - Chat completion v2 + Chat completion Args: messages: An array of message objects that form the conversation history or context for @@ -533,13 +482,6 @@ async def chat( temperature results in more varied and less predictable text, while a lower temperature produces more deterministic and conservative outputs. - tool_choice: Configure how the model will call functions: `auto` will allow the model to - automatically choose the best tool, `none` disables tool calling. You can also - pass a specific previously defined function as a string. - - tools: An array of tools described to the model using JSON schema that the model can - use to generate responses. - top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with cumulative probability above this threshold are considered, controlling the @@ -566,8 +508,6 @@ async def chat( stop: Union[List[str], str] | NotGiven = NOT_GIVEN, stream: Literal[False] | Literal[True] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, - tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -587,8 +527,6 @@ async def chat( "stop": stop, "stream": stream, "temperature": temperature, - "tool_choice": tool_choice, - "tools": tools, "top_p": top_p, }, chat_chat_params.ChatChatParams, diff --git a/src/writerai/types/chat.py b/src/writerai/types/chat.py index 834f1d60..3ff233f8 100644 --- a/src/writerai/types/chat.py +++ b/src/writerai/types/chat.py @@ -1,27 +1,11 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List, Optional +from typing import List from typing_extensions import Literal from .._models import BaseModel -__all__ = ["Chat", "Choice", "ChoiceMessage", "ChoiceMessageToolCall", "ChoiceMessageToolCallFunction"] - - -class ChoiceMessageToolCallFunction(BaseModel): - arguments: Optional[str] = None - - name: Optional[str] = None - - -class ChoiceMessageToolCall(BaseModel): - id: Optional[str] = None - - function: Optional[ChoiceMessageToolCallFunction] = None - - index: Optional[int] = None - - type: Optional[str] = None +__all__ = ["Chat", "Choice", "ChoiceMessage"] class ChoiceMessage(BaseModel): @@ -39,11 +23,9 @@ class ChoiceMessage(BaseModel): output within the interaction flow. """ - tool_calls: Optional[List[ChoiceMessageToolCall]] = None - class Choice(BaseModel): - finish_reason: Literal["stop", "length", "content_filter", "tool_calls"] + finish_reason: Literal["stop", "length", "content_filter"] """Describes the condition under which the model ceased generating content. Common reasons include 'length' (reached the maximum output size), 'stop' diff --git a/src/writerai/types/chat_chat_params.py b/src/writerai/types/chat_chat_params.py index 96d27aa6..522f7998 100644 --- a/src/writerai/types/chat_chat_params.py +++ b/src/writerai/types/chat_chat_params.py @@ -3,19 +3,9 @@ from __future__ import annotations from typing import List, Union, Iterable -from typing_extensions import Literal, Required, TypeAlias, TypedDict +from typing_extensions import Literal, Required, TypedDict -__all__ = [ - "ChatChatParamsBase", - "Message", - "ToolChoice", - "ToolChoiceJsonObjectToolChoice", - "ToolChoiceStringToolChoice", - "Tool", - "ToolFunction", - "ChatChatParamsNonStreaming", - "ChatChatParamsStreaming", -] +__all__ = ["ChatChatParamsBase", "Message", "ChatChatParamsNonStreaming", "ChatChatParamsStreaming"] class ChatChatParamsBase(TypedDict, total=False): @@ -59,19 +49,6 @@ class ChatChatParamsBase(TypedDict, total=False): lower temperature produces more deterministic and conservative outputs. """ - tool_choice: ToolChoice - """ - Configure how the model will call functions: `auto` will allow the model to - automatically choose the best tool, `none` disables tool calling. You can also - pass a specific previously defined function as a string. - """ - - tools: Iterable[Tool] - """ - An array of tools described to the model using JSON schema that the model can - use to generate responses. - """ - top_p: float """ Sets the threshold for "nucleus sampling," a technique to focus the model's @@ -89,31 +66,6 @@ class Message(TypedDict, total=False): name: str -class ToolChoiceJsonObjectToolChoice(TypedDict, total=False): - value: Required[object] - - -class ToolChoiceStringToolChoice(TypedDict, total=False): - value: Required[Literal["none", "auto", "required"]] - - -ToolChoice: TypeAlias = Union[ToolChoiceJsonObjectToolChoice, ToolChoiceStringToolChoice] - - -class ToolFunction(TypedDict, total=False): - name: Required[str] - - description: str - - parameters: object - - -class Tool(TypedDict, total=False): - function: Required[ToolFunction] - - type: Required[str] - - class ChatChatParamsNonStreaming(ChatChatParamsBase, total=False): stream: Literal[False] """ diff --git a/tests/api_resources/test_chat.py b/tests/api_resources/test_chat.py index f814782a..b7a5caf4 100644 --- a/tests/api_resources/test_chat.py +++ b/tests/api_resources/test_chat.py @@ -46,33 +46,6 @@ def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: stop=["string", "string", "string"], stream=False, temperature=0, - tool_choice={"value": {}}, - tools=[ - { - "function": { - "name": "name", - "description": "description", - "parameters": {}, - }, - "type": "type", - }, - { - "function": { - "name": "name", - "description": "description", - "parameters": {}, - }, - "type": "type", - }, - { - "function": { - "name": "name", - "description": "description", - "parameters": {}, - }, - "type": "type", - }, - ], top_p=0, ) assert_matches_type(Chat, chat, path=["response"]) @@ -143,33 +116,6 @@ def test_method_chat_with_all_params_overload_2(self, client: Writer) -> None: n=0, stop=["string", "string", "string"], temperature=0, - tool_choice={"value": {}}, - tools=[ - { - "function": { - "name": "name", - "description": "description", - "parameters": {}, - }, - "type": "type", - }, - { - "function": { - "name": "name", - "description": "description", - "parameters": {}, - }, - "type": "type", - }, - { - "function": { - "name": "name", - "description": "description", - "parameters": {}, - }, - "type": "type", - }, - ], top_p=0, ) chat_stream.response.close() @@ -244,33 +190,6 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW stop=["string", "string", "string"], stream=False, temperature=0, - tool_choice={"value": {}}, - tools=[ - { - "function": { - "name": "name", - "description": "description", - "parameters": {}, - }, - "type": "type", - }, - { - "function": { - "name": "name", - "description": "description", - "parameters": {}, - }, - "type": "type", - }, - { - "function": { - "name": "name", - "description": "description", - "parameters": {}, - }, - "type": "type", - }, - ], top_p=0, ) assert_matches_type(Chat, chat, path=["response"]) @@ -341,33 +260,6 @@ async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncW n=0, stop=["string", "string", "string"], temperature=0, - tool_choice={"value": {}}, - tools=[ - { - "function": { - "name": "name", - "description": "description", - "parameters": {}, - }, - "type": "type", - }, - { - "function": { - "name": "name", - "description": "description", - "parameters": {}, - }, - "type": "type", - }, - { - "function": { - "name": "name", - "description": "description", - "parameters": {}, - }, - "type": "type", - }, - ], top_p=0, ) await chat_stream.response.aclose() From c4b54d0df705e225197972a71968d8820898a9fa Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Sep 2024 16:53:52 +0000 Subject: [PATCH 076/399] feat(api): manual updates (#71) --- .stats.yml | 2 +- api.md | 5 +- src/writerai/resources/files.py | 83 +++++++++++- src/writerai/resources/graphs.py | 121 ++++++++++++++++++ src/writerai/types/__init__.py | 3 + src/writerai/types/file_retry_params.py | 13 ++ src/writerai/types/graph_question_params.py | 26 ++++ src/writerai/types/graph_question_response.py | 45 +++++++ tests/api_resources/test_files.py | 86 +++++++++++++ tests/api_resources/test_graphs.py | 81 ++++++++++++ 10 files changed, 462 insertions(+), 3 deletions(-) create mode 100644 src/writerai/types/file_retry_params.py create mode 100644 src/writerai/types/graph_question_params.py create mode 100644 src/writerai/types/graph_question_response.py diff --git a/.stats.yml b/.stats.yml index a703388b..f9693fc9 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ -configured_endpoints: 16 +configured_endpoints: 18 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-5d34056e0e749d39a81c92ed4fb221da6ac627cff4cb9edcb6b054fa00abb335.yml diff --git a/api.md b/api.md index 0d13ba2a..87f63eb6 100644 --- a/api.md +++ b/api.md @@ -56,6 +56,7 @@ from writerai.types import ( GraphCreateResponse, GraphUpdateResponse, GraphDeleteResponse, + GraphQuestionResponse, GraphRemoveFileFromGraphResponse, ) ``` @@ -68,6 +69,7 @@ Methods: - client.graphs.list(\*\*params) -> SyncCursorPage[Graph] - client.graphs.delete(graph_id) -> GraphDeleteResponse - client.graphs.add_file_to_graph(graph_id, \*\*params) -> File +- client.graphs.question(\*\*params) -> GraphQuestionResponse - client.graphs.remove_file_from_graph(file_id, \*, graph_id) -> GraphRemoveFileFromGraphResponse # Files @@ -75,7 +77,7 @@ Methods: Types: ```python -from writerai.types import File, FileDeleteResponse +from writerai.types import File, FileDeleteResponse, FileRetryResponse ``` Methods: @@ -84,4 +86,5 @@ Methods: - client.files.list(\*\*params) -> SyncCursorPage[File] - client.files.delete(file_id) -> FileDeleteResponse - client.files.download(file_id) -> BinaryAPIResponse +- client.files.retry(\*\*params) -> object - client.files.upload(\*\*params) -> File diff --git a/src/writerai/resources/files.py b/src/writerai/resources/files.py index fc8ebe28..ab5e3d84 100644 --- a/src/writerai/resources/files.py +++ b/src/writerai/resources/files.py @@ -2,11 +2,12 @@ from __future__ import annotations +from typing import List from typing_extensions import Literal import httpx -from ..types import file_list_params, file_upload_params +from ..types import file_list_params, file_retry_params, file_upload_params from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes from .._utils import ( maybe_transform, @@ -225,6 +226,40 @@ def download( cast_to=BinaryAPIResponse, ) + def retry( + self, + *, + file_ids: List[str], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> object: + """ + Retry failed files + + Args: + file_ids: The unique identifier of the files to be retried. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/v1/files/retry", + body=maybe_transform({"file_ids": file_ids}, file_retry_params.FileRetryParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=object, + ) + def upload( self, *, @@ -449,6 +484,40 @@ async def download( cast_to=AsyncBinaryAPIResponse, ) + async def retry( + self, + *, + file_ids: List[str], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> object: + """ + Retry failed files + + Args: + file_ids: The unique identifier of the files to be retried. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/v1/files/retry", + body=await async_maybe_transform({"file_ids": file_ids}, file_retry_params.FileRetryParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=object, + ) + async def upload( self, *, @@ -501,6 +570,9 @@ def __init__(self, files: FilesResource) -> None: files.download, BinaryAPIResponse, ) + self.retry = to_raw_response_wrapper( + files.retry, + ) self.upload = to_raw_response_wrapper( files.upload, ) @@ -523,6 +595,9 @@ def __init__(self, files: AsyncFilesResource) -> None: files.download, AsyncBinaryAPIResponse, ) + self.retry = async_to_raw_response_wrapper( + files.retry, + ) self.upload = async_to_raw_response_wrapper( files.upload, ) @@ -545,6 +620,9 @@ def __init__(self, files: FilesResource) -> None: files.download, StreamedBinaryAPIResponse, ) + self.retry = to_streamed_response_wrapper( + files.retry, + ) self.upload = to_streamed_response_wrapper( files.upload, ) @@ -567,6 +645,9 @@ def __init__(self, files: AsyncFilesResource) -> None: files.download, AsyncStreamedBinaryAPIResponse, ) + self.retry = async_to_streamed_response_wrapper( + files.retry, + ) self.upload = async_to_streamed_response_wrapper( files.upload, ) diff --git a/src/writerai/resources/graphs.py b/src/writerai/resources/graphs.py index 76c84071..880efded 100644 --- a/src/writerai/resources/graphs.py +++ b/src/writerai/resources/graphs.py @@ -2,6 +2,7 @@ from __future__ import annotations +from typing import List from typing_extensions import Literal import httpx @@ -10,6 +11,7 @@ graph_list_params, graph_create_params, graph_update_params, + graph_question_params, graph_add_file_to_graph_params, ) from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven @@ -32,6 +34,7 @@ from ..types.graph_create_response import GraphCreateResponse from ..types.graph_delete_response import GraphDeleteResponse from ..types.graph_update_response import GraphUpdateResponse +from ..types.graph_question_response import GraphQuestionResponse from ..types.graph_remove_file_from_graph_response import GraphRemoveFileFromGraphResponse __all__ = ["GraphsResource", "AsyncGraphsResource"] @@ -311,6 +314,59 @@ def add_file_to_graph( cast_to=File, ) + def question( + self, + *, + graph_ids: List[str], + question: str, + stream: bool, + subqueries: bool, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> GraphQuestionResponse: + """ + Knowledge Graph question + + Args: + graph_ids: The unique identifiers of the Knowledge Graphs to be queried. + + question: The question to be answered using the Knowledge Graph. + + stream: Determines whether the model's output should be streamed. If true, the output is + generated and sent incrementally, which can be useful for real-time + applications. + + subqueries: Specify whether to include subqueries. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/v1/graphs/question", + body=maybe_transform( + { + "graph_ids": graph_ids, + "question": question, + "stream": stream, + "subqueries": subqueries, + }, + graph_question_params.GraphQuestionParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GraphQuestionResponse, + ) + def remove_file_from_graph( self, file_id: str, @@ -624,6 +680,59 @@ async def add_file_to_graph( cast_to=File, ) + async def question( + self, + *, + graph_ids: List[str], + question: str, + stream: bool, + subqueries: bool, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> GraphQuestionResponse: + """ + Knowledge Graph question + + Args: + graph_ids: The unique identifiers of the Knowledge Graphs to be queried. + + question: The question to be answered using the Knowledge Graph. + + stream: Determines whether the model's output should be streamed. If true, the output is + generated and sent incrementally, which can be useful for real-time + applications. + + subqueries: Specify whether to include subqueries. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/v1/graphs/question", + body=await async_maybe_transform( + { + "graph_ids": graph_ids, + "question": question, + "stream": stream, + "subqueries": subqueries, + }, + graph_question_params.GraphQuestionParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GraphQuestionResponse, + ) + async def remove_file_from_graph( self, file_id: str, @@ -683,6 +792,9 @@ def __init__(self, graphs: GraphsResource) -> None: self.add_file_to_graph = to_raw_response_wrapper( graphs.add_file_to_graph, ) + self.question = to_raw_response_wrapper( + graphs.question, + ) self.remove_file_from_graph = to_raw_response_wrapper( graphs.remove_file_from_graph, ) @@ -710,6 +822,9 @@ def __init__(self, graphs: AsyncGraphsResource) -> None: self.add_file_to_graph = async_to_raw_response_wrapper( graphs.add_file_to_graph, ) + self.question = async_to_raw_response_wrapper( + graphs.question, + ) self.remove_file_from_graph = async_to_raw_response_wrapper( graphs.remove_file_from_graph, ) @@ -737,6 +852,9 @@ def __init__(self, graphs: GraphsResource) -> None: self.add_file_to_graph = to_streamed_response_wrapper( graphs.add_file_to_graph, ) + self.question = to_streamed_response_wrapper( + graphs.question, + ) self.remove_file_from_graph = to_streamed_response_wrapper( graphs.remove_file_from_graph, ) @@ -764,6 +882,9 @@ def __init__(self, graphs: AsyncGraphsResource) -> None: self.add_file_to_graph = async_to_streamed_response_wrapper( graphs.add_file_to_graph, ) + self.question = async_to_streamed_response_wrapper( + graphs.question, + ) self.remove_file_from_graph = async_to_streamed_response_wrapper( graphs.remove_file_from_graph, ) diff --git a/src/writerai/types/__init__.py b/src/writerai/types/__init__.py index d76104cb..78172c5d 100644 --- a/src/writerai/types/__init__.py +++ b/src/writerai/types/__init__.py @@ -9,6 +9,7 @@ from .streaming_data import StreamingData as StreamingData from .chat_chat_params import ChatChatParams as ChatChatParams from .file_list_params import FileListParams as FileListParams +from .file_retry_params import FileRetryParams as FileRetryParams from .graph_list_params import GraphListParams as GraphListParams from .file_upload_params import FileUploadParams as FileUploadParams from .chat_streaming_data import ChatStreamingData as ChatStreamingData @@ -18,7 +19,9 @@ from .file_delete_response import FileDeleteResponse as FileDeleteResponse from .graph_create_response import GraphCreateResponse as GraphCreateResponse from .graph_delete_response import GraphDeleteResponse as GraphDeleteResponse +from .graph_question_params import GraphQuestionParams as GraphQuestionParams from .graph_update_response import GraphUpdateResponse as GraphUpdateResponse +from .graph_question_response import GraphQuestionResponse as GraphQuestionResponse from .completion_create_params import CompletionCreateParams as CompletionCreateParams from .graph_add_file_to_graph_params import GraphAddFileToGraphParams as GraphAddFileToGraphParams from .application_generate_content_params import ApplicationGenerateContentParams as ApplicationGenerateContentParams diff --git a/src/writerai/types/file_retry_params.py b/src/writerai/types/file_retry_params.py new file mode 100644 index 00000000..a086d374 --- /dev/null +++ b/src/writerai/types/file_retry_params.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List +from typing_extensions import Required, TypedDict + +__all__ = ["FileRetryParams"] + + +class FileRetryParams(TypedDict, total=False): + file_ids: Required[List[str]] + """The unique identifier of the files to be retried.""" diff --git a/src/writerai/types/graph_question_params.py b/src/writerai/types/graph_question_params.py new file mode 100644 index 00000000..acbc4fc1 --- /dev/null +++ b/src/writerai/types/graph_question_params.py @@ -0,0 +1,26 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List +from typing_extensions import Required, TypedDict + +__all__ = ["GraphQuestionParams"] + + +class GraphQuestionParams(TypedDict, total=False): + graph_ids: Required[List[str]] + """The unique identifiers of the Knowledge Graphs to be queried.""" + + question: Required[str] + """The question to be answered using the Knowledge Graph.""" + + stream: Required[bool] + """Determines whether the model's output should be streamed. + + If true, the output is generated and sent incrementally, which can be useful for + real-time applications. + """ + + subqueries: Required[bool] + """Specify whether to include subqueries.""" diff --git a/src/writerai/types/graph_question_response.py b/src/writerai/types/graph_question_response.py new file mode 100644 index 00000000..61416781 --- /dev/null +++ b/src/writerai/types/graph_question_response.py @@ -0,0 +1,45 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .._models import BaseModel + +__all__ = ["GraphQuestionResponse", "Source", "Subquery", "SubquerySource"] + + +class Source(BaseModel): + file_id: str + """The unique identifier of the file.""" + + snippet: str + """A snippet of text from the source file.""" + + +class SubquerySource(BaseModel): + file_id: str + """The unique identifier of the file.""" + + snippet: str + """A snippet of text from the source file.""" + + +class Subquery(BaseModel): + answer: str + """The answer to the subquery.""" + + query: str + """The subquery that was asked.""" + + sources: List[SubquerySource] + + +class GraphQuestionResponse(BaseModel): + answer: str + """The answer to the question.""" + + question: str + """The question that was asked.""" + + sources: List[Source] + + subqueries: Optional[List[Subquery]] = None diff --git a/tests/api_resources/test_files.py b/tests/api_resources/test_files.py index 45cbb33f..36f91747 100644 --- a/tests/api_resources/test_files.py +++ b/tests/api_resources/test_files.py @@ -193,6 +193,49 @@ def test_path_params_download(self, client: Writer) -> None: "", ) + @parametrize + def test_method_retry(self, client: Writer) -> None: + file = client.files.retry( + file_ids=[ + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ], + ) + assert_matches_type(object, file, path=["response"]) + + @parametrize + def test_raw_response_retry(self, client: Writer) -> None: + response = client.files.with_raw_response.retry( + file_ids=[ + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + file = response.parse() + assert_matches_type(object, file, path=["response"]) + + @parametrize + def test_streaming_response_retry(self, client: Writer) -> None: + with client.files.with_streaming_response.retry( + file_ids=[ + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + file = response.parse() + assert_matches_type(object, file, path=["response"]) + + assert cast(Any, response.is_closed) is True + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") @parametrize def test_method_upload(self, client: Writer) -> None: @@ -401,6 +444,49 @@ async def test_path_params_download(self, async_client: AsyncWriter) -> None: "", ) + @parametrize + async def test_method_retry(self, async_client: AsyncWriter) -> None: + file = await async_client.files.retry( + file_ids=[ + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ], + ) + assert_matches_type(object, file, path=["response"]) + + @parametrize + async def test_raw_response_retry(self, async_client: AsyncWriter) -> None: + response = await async_client.files.with_raw_response.retry( + file_ids=[ + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + file = await response.parse() + assert_matches_type(object, file, path=["response"]) + + @parametrize + async def test_streaming_response_retry(self, async_client: AsyncWriter) -> None: + async with async_client.files.with_streaming_response.retry( + file_ids=[ + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + file = await response.parse() + assert_matches_type(object, file, path=["response"]) + + assert cast(Any, response.is_closed) is True + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") @parametrize async def test_method_upload(self, async_client: AsyncWriter) -> None: diff --git a/tests/api_resources/test_graphs.py b/tests/api_resources/test_graphs.py index 0cd2383e..b7c82e20 100644 --- a/tests/api_resources/test_graphs.py +++ b/tests/api_resources/test_graphs.py @@ -15,6 +15,7 @@ GraphCreateResponse, GraphDeleteResponse, GraphUpdateResponse, + GraphQuestionResponse, GraphRemoveFileFromGraphResponse, ) from writerai.pagination import SyncCursorPage, AsyncCursorPage @@ -258,6 +259,46 @@ def test_path_params_add_file_to_graph(self, client: Writer) -> None: file_id="file_id", ) + @parametrize + def test_method_question(self, client: Writer) -> None: + graph = client.graphs.question( + graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], + question="question", + stream=True, + subqueries=True, + ) + assert_matches_type(GraphQuestionResponse, graph, path=["response"]) + + @parametrize + def test_raw_response_question(self, client: Writer) -> None: + response = client.graphs.with_raw_response.question( + graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], + question="question", + stream=True, + subqueries=True, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + graph = response.parse() + assert_matches_type(GraphQuestionResponse, graph, path=["response"]) + + @parametrize + def test_streaming_response_question(self, client: Writer) -> None: + with client.graphs.with_streaming_response.question( + graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], + question="question", + stream=True, + subqueries=True, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + graph = response.parse() + assert_matches_type(GraphQuestionResponse, graph, path=["response"]) + + assert cast(Any, response.is_closed) is True + @parametrize def test_method_remove_file_from_graph(self, client: Writer) -> None: graph = client.graphs.remove_file_from_graph( @@ -543,6 +584,46 @@ async def test_path_params_add_file_to_graph(self, async_client: AsyncWriter) -> file_id="file_id", ) + @parametrize + async def test_method_question(self, async_client: AsyncWriter) -> None: + graph = await async_client.graphs.question( + graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], + question="question", + stream=True, + subqueries=True, + ) + assert_matches_type(GraphQuestionResponse, graph, path=["response"]) + + @parametrize + async def test_raw_response_question(self, async_client: AsyncWriter) -> None: + response = await async_client.graphs.with_raw_response.question( + graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], + question="question", + stream=True, + subqueries=True, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + graph = await response.parse() + assert_matches_type(GraphQuestionResponse, graph, path=["response"]) + + @parametrize + async def test_streaming_response_question(self, async_client: AsyncWriter) -> None: + async with async_client.graphs.with_streaming_response.question( + graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], + question="question", + stream=True, + subqueries=True, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + graph = await response.parse() + assert_matches_type(GraphQuestionResponse, graph, path=["response"]) + + assert cast(Any, response.is_closed) is True + @parametrize async def test_method_remove_file_from_graph(self, async_client: AsyncWriter) -> None: graph = await async_client.graphs.remove_file_from_graph( From 883a27f81920ec7e6ce095a912e82021bb48db14 Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Tue, 24 Sep 2024 18:28:20 +0000 Subject: [PATCH 077/399] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index f9693fc9..f69c5140 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-5d34056e0e749d39a81c92ed4fb221da6ac627cff4cb9edcb6b054fa00abb335.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-8faeea37720f7614ef560a87d44dc13a96672b12e14d04bce642dee5d6939c35.yml From e8899cfdd85fc3b10cdcf12198a7408c0d46ec18 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Sep 2024 19:39:39 +0000 Subject: [PATCH 078/399] chore(internal): version bump (#73) --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index ac031714..1b77f506 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.6.1" + ".": "0.7.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index ac12f3e7..f9d07992 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "0.6.1" +version = "0.7.0" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index 7d5db693..e4b348eb 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "0.6.1" # x-release-please-version +__version__ = "0.7.0" # x-release-please-version From e807c1fce384b5feb8835a42143109768c1869ea Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 26 Sep 2024 11:41:43 +0000 Subject: [PATCH 079/399] feat(api): add model graphs.Question (#74) --- api.md | 4 ++-- src/writerai/resources/graphs.py | 10 +++++----- src/writerai/types/__init__.py | 2 +- .../{graph_question_response.py => question.py} | 4 ++-- tests/api_resources/test_graphs.py | 14 +++++++------- 5 files changed, 17 insertions(+), 17 deletions(-) rename src/writerai/types/{graph_question_response.py => question.py} (87%) diff --git a/api.md b/api.md index 87f63eb6..0bebe728 100644 --- a/api.md +++ b/api.md @@ -53,10 +53,10 @@ Types: ```python from writerai.types import ( Graph, + Question, GraphCreateResponse, GraphUpdateResponse, GraphDeleteResponse, - GraphQuestionResponse, GraphRemoveFileFromGraphResponse, ) ``` @@ -69,7 +69,7 @@ Methods: - client.graphs.list(\*\*params) -> SyncCursorPage[Graph] - client.graphs.delete(graph_id) -> GraphDeleteResponse - client.graphs.add_file_to_graph(graph_id, \*\*params) -> File -- client.graphs.question(\*\*params) -> GraphQuestionResponse +- client.graphs.question(\*\*params) -> Question - client.graphs.remove_file_from_graph(file_id, \*, graph_id) -> GraphRemoveFileFromGraphResponse # Files diff --git a/src/writerai/resources/graphs.py b/src/writerai/resources/graphs.py index 880efded..d7c647ee 100644 --- a/src/writerai/resources/graphs.py +++ b/src/writerai/resources/graphs.py @@ -31,10 +31,10 @@ from ..types.file import File from ..types.graph import Graph from .._base_client import AsyncPaginator, make_request_options +from ..types.question import Question from ..types.graph_create_response import GraphCreateResponse from ..types.graph_delete_response import GraphDeleteResponse from ..types.graph_update_response import GraphUpdateResponse -from ..types.graph_question_response import GraphQuestionResponse from ..types.graph_remove_file_from_graph_response import GraphRemoveFileFromGraphResponse __all__ = ["GraphsResource", "AsyncGraphsResource"] @@ -327,7 +327,7 @@ def question( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> GraphQuestionResponse: + ) -> Question: """ Knowledge Graph question @@ -364,7 +364,7 @@ def question( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=GraphQuestionResponse, + cast_to=Question, ) def remove_file_from_graph( @@ -693,7 +693,7 @@ async def question( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> GraphQuestionResponse: + ) -> Question: """ Knowledge Graph question @@ -730,7 +730,7 @@ async def question( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=GraphQuestionResponse, + cast_to=Question, ) async def remove_file_from_graph( diff --git a/src/writerai/types/__init__.py b/src/writerai/types/__init__.py index 78172c5d..afa3ce44 100644 --- a/src/writerai/types/__init__.py +++ b/src/writerai/types/__init__.py @@ -5,6 +5,7 @@ from .chat import Chat as Chat from .file import File as File from .graph import Graph as Graph +from .question import Question as Question from .completion import Completion as Completion from .streaming_data import StreamingData as StreamingData from .chat_chat_params import ChatChatParams as ChatChatParams @@ -21,7 +22,6 @@ from .graph_delete_response import GraphDeleteResponse as GraphDeleteResponse from .graph_question_params import GraphQuestionParams as GraphQuestionParams from .graph_update_response import GraphUpdateResponse as GraphUpdateResponse -from .graph_question_response import GraphQuestionResponse as GraphQuestionResponse from .completion_create_params import CompletionCreateParams as CompletionCreateParams from .graph_add_file_to_graph_params import GraphAddFileToGraphParams as GraphAddFileToGraphParams from .application_generate_content_params import ApplicationGenerateContentParams as ApplicationGenerateContentParams diff --git a/src/writerai/types/graph_question_response.py b/src/writerai/types/question.py similarity index 87% rename from src/writerai/types/graph_question_response.py rename to src/writerai/types/question.py index 61416781..24473a47 100644 --- a/src/writerai/types/graph_question_response.py +++ b/src/writerai/types/question.py @@ -4,7 +4,7 @@ from .._models import BaseModel -__all__ = ["GraphQuestionResponse", "Source", "Subquery", "SubquerySource"] +__all__ = ["Question", "Source", "Subquery", "SubquerySource"] class Source(BaseModel): @@ -33,7 +33,7 @@ class Subquery(BaseModel): sources: List[SubquerySource] -class GraphQuestionResponse(BaseModel): +class Question(BaseModel): answer: str """The answer to the question.""" diff --git a/tests/api_resources/test_graphs.py b/tests/api_resources/test_graphs.py index b7c82e20..fcc9202b 100644 --- a/tests/api_resources/test_graphs.py +++ b/tests/api_resources/test_graphs.py @@ -12,10 +12,10 @@ from writerai.types import ( File, Graph, + Question, GraphCreateResponse, GraphDeleteResponse, GraphUpdateResponse, - GraphQuestionResponse, GraphRemoveFileFromGraphResponse, ) from writerai.pagination import SyncCursorPage, AsyncCursorPage @@ -267,7 +267,7 @@ def test_method_question(self, client: Writer) -> None: stream=True, subqueries=True, ) - assert_matches_type(GraphQuestionResponse, graph, path=["response"]) + assert_matches_type(Question, graph, path=["response"]) @parametrize def test_raw_response_question(self, client: Writer) -> None: @@ -281,7 +281,7 @@ def test_raw_response_question(self, client: Writer) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" graph = response.parse() - assert_matches_type(GraphQuestionResponse, graph, path=["response"]) + assert_matches_type(Question, graph, path=["response"]) @parametrize def test_streaming_response_question(self, client: Writer) -> None: @@ -295,7 +295,7 @@ def test_streaming_response_question(self, client: Writer) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" graph = response.parse() - assert_matches_type(GraphQuestionResponse, graph, path=["response"]) + assert_matches_type(Question, graph, path=["response"]) assert cast(Any, response.is_closed) is True @@ -592,7 +592,7 @@ async def test_method_question(self, async_client: AsyncWriter) -> None: stream=True, subqueries=True, ) - assert_matches_type(GraphQuestionResponse, graph, path=["response"]) + assert_matches_type(Question, graph, path=["response"]) @parametrize async def test_raw_response_question(self, async_client: AsyncWriter) -> None: @@ -606,7 +606,7 @@ async def test_raw_response_question(self, async_client: AsyncWriter) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" graph = await response.parse() - assert_matches_type(GraphQuestionResponse, graph, path=["response"]) + assert_matches_type(Question, graph, path=["response"]) @parametrize async def test_streaming_response_question(self, async_client: AsyncWriter) -> None: @@ -620,7 +620,7 @@ async def test_streaming_response_question(self, async_client: AsyncWriter) -> N assert response.http_request.headers.get("X-Stainless-Lang") == "python" graph = await response.parse() - assert_matches_type(GraphQuestionResponse, graph, path=["response"]) + assert_matches_type(Question, graph, path=["response"]) assert cast(Any, response.is_closed) is True From 19c08a1e7a4e0910e28e1452e79f66f0cd204c78 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 26 Sep 2024 11:43:32 +0000 Subject: [PATCH 080/399] chore(internal): codegen related update (#76) --- src/writerai/_base_client.py | 5 +- tests/test_client.py | 130 +++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 1 deletion(-) diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index 762a7c41..bf35ba1f 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -412,7 +412,10 @@ def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0 if idempotency_header and options.method.lower() != "get" and idempotency_header not in headers: headers[idempotency_header] = options.idempotency_key or self._idempotency_key() - headers.setdefault("x-stainless-retry-count", str(retries_taken)) + # Don't set the retry count header if it was already set or removed by the caller. We check + # `custom_headers`, which can contain `Omit()`, instead of `headers` to account for the removal case. + if "x-stainless-retry-count" not in (header.lower() for header in custom_headers): + headers["x-stainless-retry-count"] = str(retries_taken) return headers diff --git a/tests/test_client.py b/tests/test_client.py index 86b32f06..3c9c1cef 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -795,6 +795,70 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("writerai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + def test_omit_retry_count_header( + self, client: Writer, failures_before_success: int, respx_mock: MockRouter + ) -> None: + client = client.with_options(max_retries=4) + + nb_retries = 0 + + def retry_handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_retries + if nb_retries < failures_before_success: + nb_retries += 1 + return httpx.Response(500) + return httpx.Response(200) + + respx_mock.post("/v1/chat").mock(side_effect=retry_handler) + + response = client.chat.with_raw_response.chat( + messages=[ + { + "content": "Write a memo summarizing this earnings report.", + "role": "user", + } + ], + model="palmyra-x-004", + extra_headers={"x-stainless-retry-count": Omit()}, + ) + + assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 + + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("writerai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + def test_overwrite_retry_count_header( + self, client: Writer, failures_before_success: int, respx_mock: MockRouter + ) -> None: + client = client.with_options(max_retries=4) + + nb_retries = 0 + + def retry_handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_retries + if nb_retries < failures_before_success: + nb_retries += 1 + return httpx.Response(500) + return httpx.Response(200) + + respx_mock.post("/v1/chat").mock(side_effect=retry_handler) + + response = client.chat.with_raw_response.chat( + messages=[ + { + "content": "Write a memo summarizing this earnings report.", + "role": "user", + } + ], + model="palmyra-x-004", + extra_headers={"x-stainless-retry-count": "42"}, + ) + + assert response.http_request.headers.get("x-stainless-retry-count") == "42" + class TestAsyncWriter: client = AsyncWriter(base_url=base_url, api_key=api_key, _strict_response_validation=True) @@ -1559,3 +1623,69 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success + + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("writerai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + @pytest.mark.asyncio + async def test_omit_retry_count_header( + self, async_client: AsyncWriter, failures_before_success: int, respx_mock: MockRouter + ) -> None: + client = async_client.with_options(max_retries=4) + + nb_retries = 0 + + def retry_handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_retries + if nb_retries < failures_before_success: + nb_retries += 1 + return httpx.Response(500) + return httpx.Response(200) + + respx_mock.post("/v1/chat").mock(side_effect=retry_handler) + + response = await client.chat.with_raw_response.chat( + messages=[ + { + "content": "Write a memo summarizing this earnings report.", + "role": "user", + } + ], + model="palmyra-x-004", + extra_headers={"x-stainless-retry-count": Omit()}, + ) + + assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 + + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("writerai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + @pytest.mark.asyncio + async def test_overwrite_retry_count_header( + self, async_client: AsyncWriter, failures_before_success: int, respx_mock: MockRouter + ) -> None: + client = async_client.with_options(max_retries=4) + + nb_retries = 0 + + def retry_handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_retries + if nb_retries < failures_before_success: + nb_retries += 1 + return httpx.Response(500) + return httpx.Response(200) + + respx_mock.post("/v1/chat").mock(side_effect=retry_handler) + + response = await client.chat.with_raw_response.chat( + messages=[ + { + "content": "Write a memo summarizing this earnings report.", + "role": "user", + } + ], + model="palmyra-x-004", + extra_headers={"x-stainless-retry-count": "42"}, + ) + + assert response.http_request.headers.get("x-stainless-retry-count") == "42" From 198093086b75366f2e434b370962db3534f02cc9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 3 Oct 2024 22:08:46 +0000 Subject: [PATCH 081/399] docs(api): updates to API spec (#77) --- .stats.yml | 2 +- api.md | 4 +- src/writerai/resources/chat.py | 177 +++++++++++++++++--- src/writerai/resources/files.py | 15 +- src/writerai/resources/graphs.py | 36 ++--- src/writerai/types/__init__.py | 2 +- src/writerai/types/chat.py | 188 ++++++++++++++++++++-- src/writerai/types/chat_chat_params.py | 87 +++++++++- src/writerai/types/chat_streaming_data.py | 12 -- src/writerai/types/completion.py | 66 +++++--- src/writerai/types/file_retry_response.py | 12 ++ src/writerai/types/file_upload_params.py | 3 +- src/writerai/types/graph_create_params.py | 8 +- src/writerai/types/graph_update_params.py | 8 +- tests/api_resources/test_chat.py | 104 ++++++++++++ tests/api_resources/test_files.py | 30 ++-- tests/api_resources/test_graphs.py | 40 +++-- 17 files changed, 662 insertions(+), 132 deletions(-) delete mode 100644 src/writerai/types/chat_streaming_data.py create mode 100644 src/writerai/types/file_retry_response.py diff --git a/.stats.yml b/.stats.yml index f69c5140..62289476 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-8faeea37720f7614ef560a87d44dc13a96672b12e14d04bce642dee5d6939c35.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-4a11c63c7cb5d0d6e0496e2d0b1094b1b33c6fc3d28cd472547c9b745a7c57ac.yml diff --git a/api.md b/api.md index 0bebe728..e4a03a59 100644 --- a/api.md +++ b/api.md @@ -15,7 +15,7 @@ Methods: Types: ```python -from writerai.types import Chat, ChatStreamingData +from writerai.types import Chat ``` Methods: @@ -86,5 +86,5 @@ Methods: - client.files.list(\*\*params) -> SyncCursorPage[File] - client.files.delete(file_id) -> FileDeleteResponse - client.files.download(file_id) -> BinaryAPIResponse -- client.files.retry(\*\*params) -> object +- client.files.retry(\*\*params) -> FileRetryResponse - client.files.upload(\*\*params) -> File diff --git a/src/writerai/resources/chat.py b/src/writerai/resources/chat.py index 8b24b15e..aef685cf 100644 --- a/src/writerai/resources/chat.py +++ b/src/writerai/resources/chat.py @@ -25,7 +25,6 @@ from .._streaming import Stream, AsyncStream from ..types.chat import Chat from .._base_client import make_request_options -from ..types.chat_streaming_data import ChatStreamingData __all__ = ["ChatResource", "AsyncChatResource"] @@ -56,11 +55,15 @@ def chat( *, messages: Iterable[chat_chat_params.Message], model: str, + logprobs: bool | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, stop: Union[List[str], str] | NotGiven = NOT_GIVEN, stream: Literal[False] | NotGiven = NOT_GIVEN, + stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, + tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, + tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -69,8 +72,11 @@ def chat( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> Chat: - """ - Chat completion + """Generate a chat completion based on the provided messages. + + The response shown + below is for non-streaming. To learn about streaming responses, see the + [chat completion guide](/api-guides/chat-completion). Args: messages: An array of message objects that form the conversation history or context for @@ -79,6 +85,8 @@ def chat( model: Specifies the model to be used for generating responses. The chat model is always `palmyra-x-004` for conversational use. + logprobs: Specifies whether to return log probabilities of the output tokens. + max_tokens: Defines the maximum number of tokens (words and characters) that the model can generate in the response. The default value is set to 16, but it can be adjusted to allow for longer or shorter responses as needed. @@ -95,10 +103,21 @@ def chat( generated or only returned once fully complete. Streaming can be useful for providing real-time feedback in interactive applications. + stream_options: Additional options for streaming. + temperature: Controls the randomness or creativity of the model's responses. A higher temperature results in more varied and less predictable text, while a lower temperature produces more deterministic and conservative outputs. + tool_choice: Configure how the model will call functions: `auto` will allow the model to + automatically choose the best tool, `none` disables tool calling. You can also + pass a specific previously defined function as a string. + + tools: [Beta] An array of tools described to the model using JSON schema that the model + can use to generate responses. Please note that tool calling is in beta and + subject to change. Passing graph IDs will automatically use the Knowledge Graph + tool. + top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with cumulative probability above this threshold are considered, controlling the @@ -121,10 +140,14 @@ def chat( messages: Iterable[chat_chat_params.Message], model: str, stream: Literal[True], + logprobs: bool | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, stop: Union[List[str], str] | NotGiven = NOT_GIVEN, + stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, + tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, + tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -132,9 +155,12 @@ def chat( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Stream[ChatStreamingData]: - """ - Chat completion + ) -> Stream[Chat]: + """Generate a chat completion based on the provided messages. + + The response shown + below is for non-streaming. To learn about streaming responses, see the + [chat completion guide](/api-guides/chat-completion). Args: messages: An array of message objects that form the conversation history or context for @@ -147,6 +173,8 @@ def chat( generated or only returned once fully complete. Streaming can be useful for providing real-time feedback in interactive applications. + logprobs: Specifies whether to return log probabilities of the output tokens. + max_tokens: Defines the maximum number of tokens (words and characters) that the model can generate in the response. The default value is set to 16, but it can be adjusted to allow for longer or shorter responses as needed. @@ -159,10 +187,21 @@ def chat( producing further content. This can be a single token or an array of tokens, acting as a signal to end the output. + stream_options: Additional options for streaming. + temperature: Controls the randomness or creativity of the model's responses. A higher temperature results in more varied and less predictable text, while a lower temperature produces more deterministic and conservative outputs. + tool_choice: Configure how the model will call functions: `auto` will allow the model to + automatically choose the best tool, `none` disables tool calling. You can also + pass a specific previously defined function as a string. + + tools: [Beta] An array of tools described to the model using JSON schema that the model + can use to generate responses. Please note that tool calling is in beta and + subject to change. Passing graph IDs will automatically use the Knowledge Graph + tool. + top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with cumulative probability above this threshold are considered, controlling the @@ -185,10 +224,14 @@ def chat( messages: Iterable[chat_chat_params.Message], model: str, stream: bool, + logprobs: bool | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, stop: Union[List[str], str] | NotGiven = NOT_GIVEN, + stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, + tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, + tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -196,9 +239,12 @@ def chat( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Chat | Stream[ChatStreamingData]: - """ - Chat completion + ) -> Chat | Stream[Chat]: + """Generate a chat completion based on the provided messages. + + The response shown + below is for non-streaming. To learn about streaming responses, see the + [chat completion guide](/api-guides/chat-completion). Args: messages: An array of message objects that form the conversation history or context for @@ -211,6 +257,8 @@ def chat( generated or only returned once fully complete. Streaming can be useful for providing real-time feedback in interactive applications. + logprobs: Specifies whether to return log probabilities of the output tokens. + max_tokens: Defines the maximum number of tokens (words and characters) that the model can generate in the response. The default value is set to 16, but it can be adjusted to allow for longer or shorter responses as needed. @@ -223,10 +271,21 @@ def chat( producing further content. This can be a single token or an array of tokens, acting as a signal to end the output. + stream_options: Additional options for streaming. + temperature: Controls the randomness or creativity of the model's responses. A higher temperature results in more varied and less predictable text, while a lower temperature produces more deterministic and conservative outputs. + tool_choice: Configure how the model will call functions: `auto` will allow the model to + automatically choose the best tool, `none` disables tool calling. You can also + pass a specific previously defined function as a string. + + tools: [Beta] An array of tools described to the model using JSON schema that the model + can use to generate responses. Please note that tool calling is in beta and + subject to change. Passing graph IDs will automatically use the Knowledge Graph + tool. + top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with cumulative probability above this threshold are considered, controlling the @@ -248,11 +307,15 @@ def chat( *, messages: Iterable[chat_chat_params.Message], model: str, + logprobs: bool | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, stop: Union[List[str], str] | NotGiven = NOT_GIVEN, stream: Literal[False] | Literal[True] | NotGiven = NOT_GIVEN, + stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, + tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, + tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -260,18 +323,22 @@ def chat( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Chat | Stream[ChatStreamingData]: + ) -> Chat | Stream[Chat]: return self._post( "/v1/chat", body=maybe_transform( { "messages": messages, "model": model, + "logprobs": logprobs, "max_tokens": max_tokens, "n": n, "stop": stop, "stream": stream, + "stream_options": stream_options, "temperature": temperature, + "tool_choice": tool_choice, + "tools": tools, "top_p": top_p, }, chat_chat_params.ChatChatParams, @@ -281,7 +348,7 @@ def chat( ), cast_to=Chat, stream=stream or False, - stream_cls=Stream[ChatStreamingData], + stream_cls=Stream[Chat], ) @@ -311,11 +378,15 @@ async def chat( *, messages: Iterable[chat_chat_params.Message], model: str, + logprobs: bool | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, stop: Union[List[str], str] | NotGiven = NOT_GIVEN, stream: Literal[False] | NotGiven = NOT_GIVEN, + stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, + tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, + tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -324,8 +395,11 @@ async def chat( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> Chat: - """ - Chat completion + """Generate a chat completion based on the provided messages. + + The response shown + below is for non-streaming. To learn about streaming responses, see the + [chat completion guide](/api-guides/chat-completion). Args: messages: An array of message objects that form the conversation history or context for @@ -334,6 +408,8 @@ async def chat( model: Specifies the model to be used for generating responses. The chat model is always `palmyra-x-004` for conversational use. + logprobs: Specifies whether to return log probabilities of the output tokens. + max_tokens: Defines the maximum number of tokens (words and characters) that the model can generate in the response. The default value is set to 16, but it can be adjusted to allow for longer or shorter responses as needed. @@ -350,10 +426,21 @@ async def chat( generated or only returned once fully complete. Streaming can be useful for providing real-time feedback in interactive applications. + stream_options: Additional options for streaming. + temperature: Controls the randomness or creativity of the model's responses. A higher temperature results in more varied and less predictable text, while a lower temperature produces more deterministic and conservative outputs. + tool_choice: Configure how the model will call functions: `auto` will allow the model to + automatically choose the best tool, `none` disables tool calling. You can also + pass a specific previously defined function as a string. + + tools: [Beta] An array of tools described to the model using JSON schema that the model + can use to generate responses. Please note that tool calling is in beta and + subject to change. Passing graph IDs will automatically use the Knowledge Graph + tool. + top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with cumulative probability above this threshold are considered, controlling the @@ -376,10 +463,14 @@ async def chat( messages: Iterable[chat_chat_params.Message], model: str, stream: Literal[True], + logprobs: bool | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, stop: Union[List[str], str] | NotGiven = NOT_GIVEN, + stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, + tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, + tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -387,9 +478,12 @@ async def chat( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AsyncStream[ChatStreamingData]: - """ - Chat completion + ) -> AsyncStream[Chat]: + """Generate a chat completion based on the provided messages. + + The response shown + below is for non-streaming. To learn about streaming responses, see the + [chat completion guide](/api-guides/chat-completion). Args: messages: An array of message objects that form the conversation history or context for @@ -402,6 +496,8 @@ async def chat( generated or only returned once fully complete. Streaming can be useful for providing real-time feedback in interactive applications. + logprobs: Specifies whether to return log probabilities of the output tokens. + max_tokens: Defines the maximum number of tokens (words and characters) that the model can generate in the response. The default value is set to 16, but it can be adjusted to allow for longer or shorter responses as needed. @@ -414,10 +510,21 @@ async def chat( producing further content. This can be a single token or an array of tokens, acting as a signal to end the output. + stream_options: Additional options for streaming. + temperature: Controls the randomness or creativity of the model's responses. A higher temperature results in more varied and less predictable text, while a lower temperature produces more deterministic and conservative outputs. + tool_choice: Configure how the model will call functions: `auto` will allow the model to + automatically choose the best tool, `none` disables tool calling. You can also + pass a specific previously defined function as a string. + + tools: [Beta] An array of tools described to the model using JSON schema that the model + can use to generate responses. Please note that tool calling is in beta and + subject to change. Passing graph IDs will automatically use the Knowledge Graph + tool. + top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with cumulative probability above this threshold are considered, controlling the @@ -440,10 +547,14 @@ async def chat( messages: Iterable[chat_chat_params.Message], model: str, stream: bool, + logprobs: bool | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, stop: Union[List[str], str] | NotGiven = NOT_GIVEN, + stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, + tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, + tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -451,9 +562,12 @@ async def chat( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Chat | AsyncStream[ChatStreamingData]: - """ - Chat completion + ) -> Chat | AsyncStream[Chat]: + """Generate a chat completion based on the provided messages. + + The response shown + below is for non-streaming. To learn about streaming responses, see the + [chat completion guide](/api-guides/chat-completion). Args: messages: An array of message objects that form the conversation history or context for @@ -466,6 +580,8 @@ async def chat( generated or only returned once fully complete. Streaming can be useful for providing real-time feedback in interactive applications. + logprobs: Specifies whether to return log probabilities of the output tokens. + max_tokens: Defines the maximum number of tokens (words and characters) that the model can generate in the response. The default value is set to 16, but it can be adjusted to allow for longer or shorter responses as needed. @@ -478,10 +594,21 @@ async def chat( producing further content. This can be a single token or an array of tokens, acting as a signal to end the output. + stream_options: Additional options for streaming. + temperature: Controls the randomness or creativity of the model's responses. A higher temperature results in more varied and less predictable text, while a lower temperature produces more deterministic and conservative outputs. + tool_choice: Configure how the model will call functions: `auto` will allow the model to + automatically choose the best tool, `none` disables tool calling. You can also + pass a specific previously defined function as a string. + + tools: [Beta] An array of tools described to the model using JSON schema that the model + can use to generate responses. Please note that tool calling is in beta and + subject to change. Passing graph IDs will automatically use the Knowledge Graph + tool. + top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with cumulative probability above this threshold are considered, controlling the @@ -503,11 +630,15 @@ async def chat( *, messages: Iterable[chat_chat_params.Message], model: str, + logprobs: bool | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, stop: Union[List[str], str] | NotGiven = NOT_GIVEN, stream: Literal[False] | Literal[True] | NotGiven = NOT_GIVEN, + stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, + tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, + tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -515,18 +646,22 @@ async def chat( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Chat | AsyncStream[ChatStreamingData]: + ) -> Chat | AsyncStream[Chat]: return await self._post( "/v1/chat", body=await async_maybe_transform( { "messages": messages, "model": model, + "logprobs": logprobs, "max_tokens": max_tokens, "n": n, "stop": stop, "stream": stream, + "stream_options": stream_options, "temperature": temperature, + "tool_choice": tool_choice, + "tools": tools, "top_p": top_p, }, chat_chat_params.ChatChatParams, @@ -536,7 +671,7 @@ async def chat( ), cast_to=Chat, stream=stream or False, - stream_cls=AsyncStream[ChatStreamingData], + stream_cls=AsyncStream[Chat], ) diff --git a/src/writerai/resources/files.py b/src/writerai/resources/files.py index ab5e3d84..fa601519 100644 --- a/src/writerai/resources/files.py +++ b/src/writerai/resources/files.py @@ -8,7 +8,7 @@ import httpx from ..types import file_list_params, file_retry_params, file_upload_params -from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes +from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven from .._utils import ( maybe_transform, async_maybe_transform, @@ -32,6 +32,7 @@ from ..pagination import SyncCursorPage, AsyncCursorPage from ..types.file import File from .._base_client import AsyncPaginator, make_request_options +from ..types.file_retry_response import FileRetryResponse from ..types.file_delete_response import FileDeleteResponse __all__ = ["FilesResource", "AsyncFilesResource"] @@ -236,7 +237,7 @@ def retry( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> object: + ) -> FileRetryResponse: """ Retry failed files @@ -257,13 +258,13 @@ def retry( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=object, + cast_to=FileRetryResponse, ) def upload( self, *, - content: FileTypes, + content: object, content_disposition: str, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -494,7 +495,7 @@ async def retry( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> object: + ) -> FileRetryResponse: """ Retry failed files @@ -515,13 +516,13 @@ async def retry( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=object, + cast_to=FileRetryResponse, ) async def upload( self, *, - content: FileTypes, + content: object, content_disposition: str, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. diff --git a/src/writerai/resources/graphs.py b/src/writerai/resources/graphs.py index d7c647ee..0ef90c74 100644 --- a/src/writerai/resources/graphs.py +++ b/src/writerai/resources/graphs.py @@ -63,8 +63,8 @@ def with_streaming_response(self) -> GraphsResourceWithStreamingResponse: def create( self, *, + name: str, description: str | NotGiven = NOT_GIVEN, - name: str | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -75,11 +75,11 @@ def create( """Create graph Args: - description: A description of the graph. + name: The name of the graph. This can be at most 255 characters. - name: The name of the graph. This can be at most 255 characters. + description: A description of the graph. This can be at most 255 characters. extra_headers: Send extra headers @@ -93,8 +93,8 @@ def create( "/v1/graphs", body=maybe_transform( { - "description": description, "name": name, + "description": description, }, graph_create_params.GraphCreateParams, ), @@ -141,8 +141,8 @@ def update( self, graph_id: str, *, + name: str, description: str | NotGiven = NOT_GIVEN, - name: str | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -153,11 +153,11 @@ def update( """Update graph Args: - description: A description of the graph. + name: The name of the graph. This can be at most 255 characters. - name: The name of the graph. This can be at most 255 characters. + description: A description of the graph. This can be at most 255 characters. extra_headers: Send extra headers @@ -173,8 +173,8 @@ def update( f"/v1/graphs/{graph_id}", body=maybe_transform( { - "description": description, "name": name, + "description": description, }, graph_update_params.GraphUpdateParams, ), @@ -329,7 +329,7 @@ def question( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> Question: """ - Knowledge Graph question + Ask a question to specified Knowledge Graphs. Args: graph_ids: The unique identifiers of the Knowledge Graphs to be queried. @@ -427,8 +427,8 @@ def with_streaming_response(self) -> AsyncGraphsResourceWithStreamingResponse: async def create( self, *, + name: str, description: str | NotGiven = NOT_GIVEN, - name: str | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -439,11 +439,11 @@ async def create( """Create graph Args: - description: A description of the graph. + name: The name of the graph. This can be at most 255 characters. - name: The name of the graph. This can be at most 255 characters. + description: A description of the graph. This can be at most 255 characters. extra_headers: Send extra headers @@ -457,8 +457,8 @@ async def create( "/v1/graphs", body=await async_maybe_transform( { - "description": description, "name": name, + "description": description, }, graph_create_params.GraphCreateParams, ), @@ -505,8 +505,8 @@ async def update( self, graph_id: str, *, + name: str, description: str | NotGiven = NOT_GIVEN, - name: str | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -517,11 +517,11 @@ async def update( """Update graph Args: - description: A description of the graph. + name: The name of the graph. This can be at most 255 characters. - name: The name of the graph. This can be at most 255 characters. + description: A description of the graph. This can be at most 255 characters. extra_headers: Send extra headers @@ -537,8 +537,8 @@ async def update( f"/v1/graphs/{graph_id}", body=await async_maybe_transform( { - "description": description, "name": name, + "description": description, }, graph_update_params.GraphUpdateParams, ), @@ -695,7 +695,7 @@ async def question( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> Question: """ - Knowledge Graph question + Ask a question to specified Knowledge Graphs. Args: graph_ids: The unique identifiers of the Knowledge Graphs to be queried. diff --git a/src/writerai/types/__init__.py b/src/writerai/types/__init__.py index afa3ce44..f3f465e8 100644 --- a/src/writerai/types/__init__.py +++ b/src/writerai/types/__init__.py @@ -13,7 +13,7 @@ from .file_retry_params import FileRetryParams as FileRetryParams from .graph_list_params import GraphListParams as GraphListParams from .file_upload_params import FileUploadParams as FileUploadParams -from .chat_streaming_data import ChatStreamingData as ChatStreamingData +from .file_retry_response import FileRetryResponse as FileRetryResponse from .graph_create_params import GraphCreateParams as GraphCreateParams from .graph_update_params import GraphUpdateParams as GraphUpdateParams from .model_list_response import ModelListResponse as ModelListResponse diff --git a/src/writerai/types/chat.py b/src/writerai/types/chat.py index 3ff233f8..b7c4c001 100644 --- a/src/writerai/types/chat.py +++ b/src/writerai/types/chat.py @@ -1,21 +1,89 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List +from typing import List, Optional from typing_extensions import Literal from .._models import BaseModel -__all__ = ["Chat", "Choice", "ChoiceMessage"] +__all__ = [ + "Chat", + "Choice", + "ChoiceLogprobs", + "ChoiceLogprobsContent", + "ChoiceLogprobsContentTopLogprob", + "ChoiceLogprobsRefusal", + "ChoiceLogprobsRefusalTopLogprob", + "ChoiceMessage", + "ChoiceMessageToolCall", + "ChoiceMessageToolCallFunction", + "ChoiceSource", + "ChoiceSubquery", + "ChoiceSubquerySource", + "Usage", + "UsageCompletionTokensDetails", + "UsagePromptTokenDetails", +] -class ChoiceMessage(BaseModel): - content: str - """The text content produced by the model. +class ChoiceLogprobsContentTopLogprob(BaseModel): + token: str - This field contains the actual output generated, reflecting the model's response - to the input query or command. - """ + logprob: float + + bytes: Optional[List[int]] = None + + +class ChoiceLogprobsContent(BaseModel): + token: str + + logprob: float + + top_logprobs: List[ChoiceLogprobsContentTopLogprob] + + bytes: Optional[List[int]] = None + + +class ChoiceLogprobsRefusalTopLogprob(BaseModel): + token: str + + logprob: float + + bytes: Optional[List[int]] = None + + +class ChoiceLogprobsRefusal(BaseModel): + token: str + + logprob: float + + top_logprobs: List[ChoiceLogprobsRefusalTopLogprob] + + bytes: Optional[List[int]] = None + +class ChoiceLogprobs(BaseModel): + content: Optional[List[ChoiceLogprobsContent]] = None + + refusal: Optional[List[ChoiceLogprobsRefusal]] = None + + +class ChoiceMessageToolCallFunction(BaseModel): + arguments: Optional[str] = None + + name: Optional[str] = None + + +class ChoiceMessageToolCall(BaseModel): + id: Optional[str] = None + + function: Optional[ChoiceMessageToolCallFunction] = None + + index: Optional[int] = None + + type: Optional[str] = None + + +class ChoiceMessage(BaseModel): role: Literal["user", "assistant", "system"] """ Specifies the role associated with the content, indicating whether the message @@ -23,17 +91,94 @@ class ChoiceMessage(BaseModel): output within the interaction flow. """ + content: Optional[str] = None + """The text content produced by the model. + + This field contains the actual output generated, reflecting the model's response + to the input query or command. + """ + + tool_calls: Optional[List[ChoiceMessageToolCall]] = None + + +class ChoiceSource(BaseModel): + file_id: str + """The unique identifier of the file.""" + + snippet: str + """A snippet of text from the source file.""" + + +class ChoiceSubquerySource(BaseModel): + file_id: str + """The unique identifier of the file.""" + + snippet: str + """A snippet of text from the source file.""" + + +class ChoiceSubquery(BaseModel): + answer: str + """The answer to the subquery.""" + + query: str + """The subquery that was asked.""" + + sources: List[ChoiceSubquerySource] + class Choice(BaseModel): - finish_reason: Literal["stop", "length", "content_filter"] + index: int + """The index of the choice in the list of completions generated by the model.""" + + finish_reason: Optional[Literal["stop", "length", "content_filter", "tool_calls"]] = None """Describes the condition under which the model ceased generating content. Common reasons include 'length' (reached the maximum output size), 'stop' - (encountered a stop sequence), or 'content_filter' (harmful content filtered - out). + (encountered a stop sequence), 'content_filter' (harmful content filtered out), + or 'tool_calls' (encountered tool calls). + """ + + logprobs: Optional[ChoiceLogprobs] = None + """Log probability information for the choice.""" + + message: Optional[ChoiceMessage] = None + """The chat completion message from the model. + + Note: this field is deprecated for streaming. Use `delta` instead. """ - message: ChoiceMessage + sources: Optional[List[ChoiceSource]] = None + """An array of source objects that provide context for the model's response. + + Only returned when using the Knowledge Graph chat tool. + """ + + subqueries: Optional[List[ChoiceSubquery]] = None + """An array of sub-query objects that provide context for the model's response. + + Only returned when using the Knowledge Graph chat tool. + """ + + +class UsageCompletionTokensDetails(BaseModel): + reasoning_tokens: int + + +class UsagePromptTokenDetails(BaseModel): + cached_tokens: int + + +class Usage(BaseModel): + completion_tokens: int + + prompt_tokens: int + + total_tokens: int + + completion_tokens_details: Optional[UsageCompletionTokensDetails] = None + + prompt_token_details: Optional[UsagePromptTokenDetails] = None class Chat(BaseModel): @@ -59,3 +204,22 @@ class Chat(BaseModel): model: str """Identifies the specific model used to generate the response.""" + + object: str + """ + The type of object returned, which is always `chat.completion` for chat + responses. + """ + + service_tier: Optional[str] = None + """The service tier used for processing the request.""" + + system_fingerprint: Optional[str] = None + """A string representing the backend configuration that the model runs with.""" + + usage: Optional[Usage] = None + """Usage information for the chat completion response. + + Please note that at this time Knowledge Graph tool usage is not included in this + object. + """ diff --git a/src/writerai/types/chat_chat_params.py b/src/writerai/types/chat_chat_params.py index 522f7998..40ac0a13 100644 --- a/src/writerai/types/chat_chat_params.py +++ b/src/writerai/types/chat_chat_params.py @@ -3,9 +3,23 @@ from __future__ import annotations from typing import List, Union, Iterable -from typing_extensions import Literal, Required, TypedDict - -__all__ = ["ChatChatParamsBase", "Message", "ChatChatParamsNonStreaming", "ChatChatParamsStreaming"] +from typing_extensions import Literal, Required, TypeAlias, TypedDict + +__all__ = [ + "ChatChatParamsBase", + "Message", + "StreamOptions", + "ToolChoice", + "ToolChoiceJsonObjectToolChoice", + "ToolChoiceStringToolChoice", + "Tool", + "ToolFunctionTool", + "ToolFunctionToolFunction", + "ToolGraphTool", + "ToolGraphToolFunction", + "ChatChatParamsNonStreaming", + "ChatChatParamsStreaming", +] class ChatChatParamsBase(TypedDict, total=False): @@ -21,6 +35,9 @@ class ChatChatParamsBase(TypedDict, total=False): The chat model is always `palmyra-x-004` for conversational use. """ + logprobs: bool + """Specifies whether to return log probabilities of the output tokens.""" + max_tokens: int """ Defines the maximum number of tokens (words and characters) that the model can @@ -42,6 +59,9 @@ class ChatChatParamsBase(TypedDict, total=False): acting as a signal to end the output. """ + stream_options: StreamOptions + """Additional options for streaming.""" + temperature: float """Controls the randomness or creativity of the model's responses. @@ -49,6 +69,21 @@ class ChatChatParamsBase(TypedDict, total=False): lower temperature produces more deterministic and conservative outputs. """ + tool_choice: ToolChoice + """ + Configure how the model will call functions: `auto` will allow the model to + automatically choose the best tool, `none` disables tool calling. You can also + pass a specific previously defined function as a string. + """ + + tools: Iterable[Tool] + """ + [Beta] An array of tools described to the model using JSON schema that the model + can use to generate responses. Please note that tool calling is in beta and + subject to change. Passing graph IDs will automatically use the Knowledge Graph + tool. + """ + top_p: float """ Sets the threshold for "nucleus sampling," a technique to focus the model's @@ -66,6 +101,52 @@ class Message(TypedDict, total=False): name: str +class StreamOptions(TypedDict, total=False): + include_usage: Required[bool] + """Indicate whether to include usage information.""" + + +class ToolChoiceJsonObjectToolChoice(TypedDict, total=False): + value: Required[object] + + +class ToolChoiceStringToolChoice(TypedDict, total=False): + value: Required[Literal["none", "auto", "required"]] + + +ToolChoice: TypeAlias = Union[ToolChoiceJsonObjectToolChoice, ToolChoiceStringToolChoice] + + +class ToolFunctionToolFunction(TypedDict, total=False): + name: Required[str] + + description: str + + parameters: object + + +class ToolFunctionTool(TypedDict, total=False): + function: Required[ToolFunctionToolFunction] + + +class ToolGraphToolFunction(TypedDict, total=False): + graph_ids: Required[List[str]] + """An array of graph IDs to be used in the tool.""" + + subqueries: Required[bool] + """Boolean to indicate whether to include subqueries in the response.""" + + description: str + """A description of the graph content.""" + + +class ToolGraphTool(TypedDict, total=False): + function: Required[ToolGraphToolFunction] + + +Tool: TypeAlias = Union[ToolFunctionTool, ToolGraphTool] + + class ChatChatParamsNonStreaming(ChatChatParamsBase, total=False): stream: Literal[False] """ diff --git a/src/writerai/types/chat_streaming_data.py b/src/writerai/types/chat_streaming_data.py deleted file mode 100644 index 89ae7e83..00000000 --- a/src/writerai/types/chat_streaming_data.py +++ /dev/null @@ -1,12 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - - - -from .chat import Chat -from .._models import BaseModel - -__all__ = ["ChatStreamingData"] - - -class ChatStreamingData(BaseModel): - data: Chat diff --git a/src/writerai/types/completion.py b/src/writerai/types/completion.py index 3631622f..5f4e82ae 100644 --- a/src/writerai/types/completion.py +++ b/src/writerai/types/completion.py @@ -4,35 +4,57 @@ from .._models import BaseModel -__all__ = ["Completion", "Choice", "ChoiceLogProbs", "ChoiceLogProbsTopLogProb"] +__all__ = [ + "Completion", + "Choice", + "ChoiceLogProbs", + "ChoiceLogProbsContent", + "ChoiceLogProbsContentTopLogprob", + "ChoiceLogProbsRefusal", + "ChoiceLogProbsRefusalTopLogprob", +] -class ChoiceLogProbsTopLogProb(BaseModel): - additional_properties: Optional[float] = None - """For any additional_properties properties in the top_log_probs object""" +class ChoiceLogProbsContentTopLogprob(BaseModel): + token: str + logprob: float -class ChoiceLogProbs(BaseModel): - text_offset: Optional[List[int]] = None - """ - Positional indices of each token within the original input text, useful for - analysis and mapping. - """ + bytes: Optional[List[int]] = None - token_log_probs: Optional[List[float]] = None - """ - Log probabilities for each token, indicating the likelihood of each token's - occurrence. - """ - tokens: Optional[List[str]] = None - """An array of tokens that comprise the generated text.""" +class ChoiceLogProbsContent(BaseModel): + token: str - top_log_probs: Optional[List[ChoiceLogProbsTopLogProb]] = None - """ - An array of mappings for each token to its top log probabilities, showing - detailed prediction probabilities. - """ + logprob: float + + top_logprobs: List[ChoiceLogProbsContentTopLogprob] + + bytes: Optional[List[int]] = None + + +class ChoiceLogProbsRefusalTopLogprob(BaseModel): + token: str + + logprob: float + + bytes: Optional[List[int]] = None + + +class ChoiceLogProbsRefusal(BaseModel): + token: str + + logprob: float + + top_logprobs: List[ChoiceLogProbsRefusalTopLogprob] + + bytes: Optional[List[int]] = None + + +class ChoiceLogProbs(BaseModel): + content: Optional[List[ChoiceLogProbsContent]] = None + + refusal: Optional[List[ChoiceLogProbsRefusal]] = None class Choice(BaseModel): diff --git a/src/writerai/types/file_retry_response.py b/src/writerai/types/file_retry_response.py new file mode 100644 index 00000000..4414586c --- /dev/null +++ b/src/writerai/types/file_retry_response.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel + +__all__ = ["FileRetryResponse"] + + +class FileRetryResponse(BaseModel): + success: Optional[bool] = None + """Indicates whether the retry operation was successful.""" diff --git a/src/writerai/types/file_upload_params.py b/src/writerai/types/file_upload_params.py index 760021f9..e8c05d3a 100644 --- a/src/writerai/types/file_upload_params.py +++ b/src/writerai/types/file_upload_params.py @@ -4,13 +4,12 @@ from typing_extensions import Required, Annotated, TypedDict -from .._types import FileTypes from .._utils import PropertyInfo __all__ = ["FileUploadParams"] class FileUploadParams(TypedDict, total=False): - content: Required[FileTypes] + content: Required[object] content_disposition: Required[Annotated[str, PropertyInfo(alias="Content-Disposition")]] diff --git a/src/writerai/types/graph_create_params.py b/src/writerai/types/graph_create_params.py index 0c6406ac..7e30204a 100644 --- a/src/writerai/types/graph_create_params.py +++ b/src/writerai/types/graph_create_params.py @@ -2,14 +2,14 @@ from __future__ import annotations -from typing_extensions import TypedDict +from typing_extensions import Required, TypedDict __all__ = ["GraphCreateParams"] class GraphCreateParams(TypedDict, total=False): + name: Required[str] + """The name of the graph. This can be at most 255 characters.""" + description: str """A description of the graph. This can be at most 255 characters.""" - - name: str - """The name of the graph. This can be at most 255 characters.""" diff --git a/src/writerai/types/graph_update_params.py b/src/writerai/types/graph_update_params.py index cebca2c7..8c28d9e5 100644 --- a/src/writerai/types/graph_update_params.py +++ b/src/writerai/types/graph_update_params.py @@ -2,14 +2,14 @@ from __future__ import annotations -from typing_extensions import TypedDict +from typing_extensions import Required, TypedDict __all__ = ["GraphUpdateParams"] class GraphUpdateParams(TypedDict, total=False): + name: Required[str] + """The name of the graph. This can be at most 255 characters.""" + description: str """A description of the graph. This can be at most 255 characters.""" - - name: str - """The name of the graph. This can be at most 255 characters.""" diff --git a/tests/api_resources/test_chat.py b/tests/api_resources/test_chat.py index b7a5caf4..2bbe527d 100644 --- a/tests/api_resources/test_chat.py +++ b/tests/api_resources/test_chat.py @@ -41,11 +41,37 @@ def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: } ], model="palmyra-x-004", + logprobs=True, max_tokens=0, n=0, stop=["string", "string", "string"], stream=False, + stream_options={"include_usage": True}, temperature=0, + tool_choice={"value": {}}, + tools=[ + { + "function": { + "name": "name", + "description": "description", + "parameters": {}, + } + }, + { + "function": { + "name": "name", + "description": "description", + "parameters": {}, + } + }, + { + "function": { + "name": "name", + "description": "description", + "parameters": {}, + } + }, + ], top_p=0, ) assert_matches_type(Chat, chat, path=["response"]) @@ -112,10 +138,36 @@ def test_method_chat_with_all_params_overload_2(self, client: Writer) -> None: ], model="palmyra-x-004", stream=True, + logprobs=True, max_tokens=0, n=0, stop=["string", "string", "string"], + stream_options={"include_usage": True}, temperature=0, + tool_choice={"value": {}}, + tools=[ + { + "function": { + "name": "name", + "description": "description", + "parameters": {}, + } + }, + { + "function": { + "name": "name", + "description": "description", + "parameters": {}, + } + }, + { + "function": { + "name": "name", + "description": "description", + "parameters": {}, + } + }, + ], top_p=0, ) chat_stream.response.close() @@ -185,11 +237,37 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW } ], model="palmyra-x-004", + logprobs=True, max_tokens=0, n=0, stop=["string", "string", "string"], stream=False, + stream_options={"include_usage": True}, temperature=0, + tool_choice={"value": {}}, + tools=[ + { + "function": { + "name": "name", + "description": "description", + "parameters": {}, + } + }, + { + "function": { + "name": "name", + "description": "description", + "parameters": {}, + } + }, + { + "function": { + "name": "name", + "description": "description", + "parameters": {}, + } + }, + ], top_p=0, ) assert_matches_type(Chat, chat, path=["response"]) @@ -256,10 +334,36 @@ async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncW ], model="palmyra-x-004", stream=True, + logprobs=True, max_tokens=0, n=0, stop=["string", "string", "string"], + stream_options={"include_usage": True}, temperature=0, + tool_choice={"value": {}}, + tools=[ + { + "function": { + "name": "name", + "description": "description", + "parameters": {}, + } + }, + { + "function": { + "name": "name", + "description": "description", + "parameters": {}, + } + }, + { + "function": { + "name": "name", + "description": "description", + "parameters": {}, + } + }, + ], top_p=0, ) await chat_stream.response.aclose() diff --git a/tests/api_resources/test_files.py b/tests/api_resources/test_files.py index 36f91747..6afa628d 100644 --- a/tests/api_resources/test_files.py +++ b/tests/api_resources/test_files.py @@ -11,7 +11,11 @@ from writerai import Writer, AsyncWriter from tests.utils import assert_matches_type -from writerai.types import File, FileDeleteResponse +from writerai.types import ( + File, + FileRetryResponse, + FileDeleteResponse, +) from writerai._response import ( BinaryAPIResponse, AsyncBinaryAPIResponse, @@ -202,7 +206,7 @@ def test_method_retry(self, client: Writer) -> None: "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ], ) - assert_matches_type(object, file, path=["response"]) + assert_matches_type(FileRetryResponse, file, path=["response"]) @parametrize def test_raw_response_retry(self, client: Writer) -> None: @@ -217,7 +221,7 @@ def test_raw_response_retry(self, client: Writer) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" file = response.parse() - assert_matches_type(object, file, path=["response"]) + assert_matches_type(FileRetryResponse, file, path=["response"]) @parametrize def test_streaming_response_retry(self, client: Writer) -> None: @@ -232,7 +236,7 @@ def test_streaming_response_retry(self, client: Writer) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" file = response.parse() - assert_matches_type(object, file, path=["response"]) + assert_matches_type(FileRetryResponse, file, path=["response"]) assert cast(Any, response.is_closed) is True @@ -240,7 +244,7 @@ def test_streaming_response_retry(self, client: Writer) -> None: @parametrize def test_method_upload(self, client: Writer) -> None: file = client.files.upload( - content=b"raw file contents", + content={}, content_disposition="Content-Disposition", ) assert_matches_type(File, file, path=["response"]) @@ -249,7 +253,7 @@ def test_method_upload(self, client: Writer) -> None: @parametrize def test_raw_response_upload(self, client: Writer) -> None: response = client.files.with_raw_response.upload( - content=b"raw file contents", + content={}, content_disposition="Content-Disposition", ) @@ -262,7 +266,7 @@ def test_raw_response_upload(self, client: Writer) -> None: @parametrize def test_streaming_response_upload(self, client: Writer) -> None: with client.files.with_streaming_response.upload( - content=b"raw file contents", + content={}, content_disposition="Content-Disposition", ) as response: assert not response.is_closed @@ -453,7 +457,7 @@ async def test_method_retry(self, async_client: AsyncWriter) -> None: "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ], ) - assert_matches_type(object, file, path=["response"]) + assert_matches_type(FileRetryResponse, file, path=["response"]) @parametrize async def test_raw_response_retry(self, async_client: AsyncWriter) -> None: @@ -468,7 +472,7 @@ async def test_raw_response_retry(self, async_client: AsyncWriter) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" file = await response.parse() - assert_matches_type(object, file, path=["response"]) + assert_matches_type(FileRetryResponse, file, path=["response"]) @parametrize async def test_streaming_response_retry(self, async_client: AsyncWriter) -> None: @@ -483,7 +487,7 @@ async def test_streaming_response_retry(self, async_client: AsyncWriter) -> None assert response.http_request.headers.get("X-Stainless-Lang") == "python" file = await response.parse() - assert_matches_type(object, file, path=["response"]) + assert_matches_type(FileRetryResponse, file, path=["response"]) assert cast(Any, response.is_closed) is True @@ -491,7 +495,7 @@ async def test_streaming_response_retry(self, async_client: AsyncWriter) -> None @parametrize async def test_method_upload(self, async_client: AsyncWriter) -> None: file = await async_client.files.upload( - content=b"raw file contents", + content={}, content_disposition="Content-Disposition", ) assert_matches_type(File, file, path=["response"]) @@ -500,7 +504,7 @@ async def test_method_upload(self, async_client: AsyncWriter) -> None: @parametrize async def test_raw_response_upload(self, async_client: AsyncWriter) -> None: response = await async_client.files.with_raw_response.upload( - content=b"raw file contents", + content={}, content_disposition="Content-Disposition", ) @@ -513,7 +517,7 @@ async def test_raw_response_upload(self, async_client: AsyncWriter) -> None: @parametrize async def test_streaming_response_upload(self, async_client: AsyncWriter) -> None: async with async_client.files.with_streaming_response.upload( - content=b"raw file contents", + content={}, content_disposition="Content-Disposition", ) as response: assert not response.is_closed diff --git a/tests/api_resources/test_graphs.py b/tests/api_resources/test_graphs.py index fcc9202b..e243b436 100644 --- a/tests/api_resources/test_graphs.py +++ b/tests/api_resources/test_graphs.py @@ -28,20 +28,24 @@ class TestGraphs: @parametrize def test_method_create(self, client: Writer) -> None: - graph = client.graphs.create() + graph = client.graphs.create( + name="name", + ) assert_matches_type(GraphCreateResponse, graph, path=["response"]) @parametrize def test_method_create_with_all_params(self, client: Writer) -> None: graph = client.graphs.create( - description="description", name="name", + description="description", ) assert_matches_type(GraphCreateResponse, graph, path=["response"]) @parametrize def test_raw_response_create(self, client: Writer) -> None: - response = client.graphs.with_raw_response.create() + response = client.graphs.with_raw_response.create( + name="name", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -50,7 +54,9 @@ def test_raw_response_create(self, client: Writer) -> None: @parametrize def test_streaming_response_create(self, client: Writer) -> None: - with client.graphs.with_streaming_response.create() as response: + with client.graphs.with_streaming_response.create( + name="name", + ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -101,6 +107,7 @@ def test_path_params_retrieve(self, client: Writer) -> None: def test_method_update(self, client: Writer) -> None: graph = client.graphs.update( graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", ) assert_matches_type(GraphUpdateResponse, graph, path=["response"]) @@ -108,8 +115,8 @@ def test_method_update(self, client: Writer) -> None: def test_method_update_with_all_params(self, client: Writer) -> None: graph = client.graphs.update( graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - description="description", name="name", + description="description", ) assert_matches_type(GraphUpdateResponse, graph, path=["response"]) @@ -117,6 +124,7 @@ def test_method_update_with_all_params(self, client: Writer) -> None: def test_raw_response_update(self, client: Writer) -> None: response = client.graphs.with_raw_response.update( graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", ) assert response.is_closed is True @@ -128,6 +136,7 @@ def test_raw_response_update(self, client: Writer) -> None: def test_streaming_response_update(self, client: Writer) -> None: with client.graphs.with_streaming_response.update( graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -142,6 +151,7 @@ def test_path_params_update(self, client: Writer) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `graph_id` but received ''"): client.graphs.with_raw_response.update( graph_id="", + name="name", ) @parametrize @@ -353,20 +363,24 @@ class TestAsyncGraphs: @parametrize async def test_method_create(self, async_client: AsyncWriter) -> None: - graph = await async_client.graphs.create() + graph = await async_client.graphs.create( + name="name", + ) assert_matches_type(GraphCreateResponse, graph, path=["response"]) @parametrize async def test_method_create_with_all_params(self, async_client: AsyncWriter) -> None: graph = await async_client.graphs.create( - description="description", name="name", + description="description", ) assert_matches_type(GraphCreateResponse, graph, path=["response"]) @parametrize async def test_raw_response_create(self, async_client: AsyncWriter) -> None: - response = await async_client.graphs.with_raw_response.create() + response = await async_client.graphs.with_raw_response.create( + name="name", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -375,7 +389,9 @@ async def test_raw_response_create(self, async_client: AsyncWriter) -> None: @parametrize async def test_streaming_response_create(self, async_client: AsyncWriter) -> None: - async with async_client.graphs.with_streaming_response.create() as response: + async with async_client.graphs.with_streaming_response.create( + name="name", + ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -426,6 +442,7 @@ async def test_path_params_retrieve(self, async_client: AsyncWriter) -> None: async def test_method_update(self, async_client: AsyncWriter) -> None: graph = await async_client.graphs.update( graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", ) assert_matches_type(GraphUpdateResponse, graph, path=["response"]) @@ -433,8 +450,8 @@ async def test_method_update(self, async_client: AsyncWriter) -> None: async def test_method_update_with_all_params(self, async_client: AsyncWriter) -> None: graph = await async_client.graphs.update( graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - description="description", name="name", + description="description", ) assert_matches_type(GraphUpdateResponse, graph, path=["response"]) @@ -442,6 +459,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncWriter) -> async def test_raw_response_update(self, async_client: AsyncWriter) -> None: response = await async_client.graphs.with_raw_response.update( graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", ) assert response.is_closed is True @@ -453,6 +471,7 @@ async def test_raw_response_update(self, async_client: AsyncWriter) -> None: async def test_streaming_response_update(self, async_client: AsyncWriter) -> None: async with async_client.graphs.with_streaming_response.update( graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -467,6 +486,7 @@ async def test_path_params_update(self, async_client: AsyncWriter) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `graph_id` but received ''"): await async_client.graphs.with_raw_response.update( graph_id="", + name="name", ) @parametrize From d24d802413f646aae43ae67461ab3f15e4d19721 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 3 Oct 2024 22:12:44 +0000 Subject: [PATCH 082/399] chore(internal): codegen related update (#79) --- CONTRIBUTING.md | 44 ++++++++++++++++++++++++-------------------- README.md | 4 ++++ 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4335d76c..15626148 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,9 +2,13 @@ ### With Rye -We use [Rye](https://rye.astral.sh/) to manage dependencies so we highly recommend [installing it](https://rye.astral.sh/guide/installation/) as it will automatically provision a Python environment with the expected Python version. +We use [Rye](https://rye.astral.sh/) to manage dependencies because it will automatically provision a Python environment with the expected Python version. To set it up, run: -After installing Rye, you'll just have to run this command: +```sh +$ ./scripts/bootstrap +``` + +Or [install Rye manually](https://rye.astral.sh/guide/installation/) and run: ```sh $ rye sync --all-features @@ -39,17 +43,17 @@ modify the contents of the `src/writerai/lib/` and `examples/` directories. All files in the `examples/` directory are not modified by the generator and can be freely edited or added to. -```bash +```ts # add an example to examples/.py #!/usr/bin/env -S rye run python … ``` -``` -chmod +x examples/.py +```sh +$ chmod +x examples/.py # run the example against your api -./examples/.py +$ ./examples/.py ``` ## Using the repository from source @@ -58,8 +62,8 @@ If you’d like to use the repository from source, you can either install from g To install via git: -```bash -pip install git+ssh://git@github.com/writer/writer-python.git +```sh +$ pip install git+ssh://git@github.com/writer/writer-python.git ``` Alternatively, you can build from source and install the wheel file: @@ -68,29 +72,29 @@ Building this package will create two files in the `dist/` directory, a `.tar.gz To create a distributable version of the library, all you have to do is run this command: -```bash -rye build +```sh +$ rye build # or -python -m build +$ python -m build ``` Then to install: ```sh -pip install ./path-to-wheel-file.whl +$ 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. -```bash +```sh # you will need npm installed -npx prism mock path/to/your/openapi.yml +$ npx prism mock path/to/your/openapi.yml ``` -```bash -rye run pytest +```sh +$ ./scripts/test ``` ## Linting and formatting @@ -100,14 +104,14 @@ This repository uses [ruff](https://github.com/astral-sh/ruff) and To lint: -```bash -rye run lint +```sh +$ ./scripts/lint ``` To format and fix all ruff issues automatically: -```bash -rye run format +```sh +$ ./scripts/format ``` ## Publishing and releases diff --git a/README.md b/README.md index fa8591f8..cefba0f8 100644 --- a/README.md +++ b/README.md @@ -403,3 +403,7 @@ print(writerai.__version__) ## Requirements Python 3.7 or higher. + +## Contributing + +See [the contributing documentation](./CONTRIBUTING.md). From d917a9c2480c6ef52e515924dfb7afc2ba937223 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 3 Oct 2024 22:13:00 +0000 Subject: [PATCH 083/399] chore(internal): codegen related update (#80) --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 15626148..920e3390 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,7 +43,7 @@ modify the contents of the `src/writerai/lib/` and `examples/` directories. All files in the `examples/` directory are not modified by the generator and can be freely edited or added to. -```ts +```py # add an example to examples/.py #!/usr/bin/env -S rye run python From 6f09341cd56dbb226bb0fa49dbad2911c11aaedc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 3 Oct 2024 22:35:46 +0000 Subject: [PATCH 084/399] feat(api): rename to chat_completion_chunk (#81) --- api.md | 2 +- src/writerai/resources/chat.py | 17 +- src/writerai/types/__init__.py | 1 + src/writerai/types/chat_completion_chunk.py | 257 ++++++++++++++++++++ 4 files changed, 268 insertions(+), 9 deletions(-) create mode 100644 src/writerai/types/chat_completion_chunk.py diff --git a/api.md b/api.md index e4a03a59..40e6fd46 100644 --- a/api.md +++ b/api.md @@ -15,7 +15,7 @@ Methods: Types: ```python -from writerai.types import Chat +from writerai.types import Chat, ChatCompletionChunk ``` Methods: diff --git a/src/writerai/resources/chat.py b/src/writerai/resources/chat.py index aef685cf..a276e985 100644 --- a/src/writerai/resources/chat.py +++ b/src/writerai/resources/chat.py @@ -25,6 +25,7 @@ from .._streaming import Stream, AsyncStream from ..types.chat import Chat from .._base_client import make_request_options +from ..types.chat_completion_chunk import ChatCompletionChunk __all__ = ["ChatResource", "AsyncChatResource"] @@ -155,7 +156,7 @@ def chat( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Stream[Chat]: + ) -> Stream[ChatCompletionChunk]: """Generate a chat completion based on the provided messages. The response shown @@ -239,7 +240,7 @@ def chat( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Chat | Stream[Chat]: + ) -> Chat | Stream[ChatCompletionChunk]: """Generate a chat completion based on the provided messages. The response shown @@ -323,7 +324,7 @@ def chat( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Chat | Stream[Chat]: + ) -> Chat | Stream[ChatCompletionChunk]: return self._post( "/v1/chat", body=maybe_transform( @@ -348,7 +349,7 @@ def chat( ), cast_to=Chat, stream=stream or False, - stream_cls=Stream[Chat], + stream_cls=Stream[ChatCompletionChunk], ) @@ -478,7 +479,7 @@ async def chat( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AsyncStream[Chat]: + ) -> AsyncStream[ChatCompletionChunk]: """Generate a chat completion based on the provided messages. The response shown @@ -562,7 +563,7 @@ async def chat( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Chat | AsyncStream[Chat]: + ) -> Chat | AsyncStream[ChatCompletionChunk]: """Generate a chat completion based on the provided messages. The response shown @@ -646,7 +647,7 @@ async def chat( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Chat | AsyncStream[Chat]: + ) -> Chat | AsyncStream[ChatCompletionChunk]: return await self._post( "/v1/chat", body=await async_maybe_transform( @@ -671,7 +672,7 @@ async def chat( ), cast_to=Chat, stream=stream or False, - stream_cls=AsyncStream[Chat], + stream_cls=AsyncStream[ChatCompletionChunk], ) diff --git a/src/writerai/types/__init__.py b/src/writerai/types/__init__.py index f3f465e8..95ddde62 100644 --- a/src/writerai/types/__init__.py +++ b/src/writerai/types/__init__.py @@ -18,6 +18,7 @@ from .graph_update_params import GraphUpdateParams as GraphUpdateParams from .model_list_response import ModelListResponse as ModelListResponse from .file_delete_response import FileDeleteResponse as FileDeleteResponse +from .chat_completion_chunk import ChatCompletionChunk as ChatCompletionChunk from .graph_create_response import GraphCreateResponse as GraphCreateResponse from .graph_delete_response import GraphDeleteResponse as GraphDeleteResponse from .graph_question_params import GraphQuestionParams as GraphQuestionParams diff --git a/src/writerai/types/chat_completion_chunk.py b/src/writerai/types/chat_completion_chunk.py new file mode 100644 index 00000000..c48667cd --- /dev/null +++ b/src/writerai/types/chat_completion_chunk.py @@ -0,0 +1,257 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = [ + "ChatCompletionChunk", + "Choice", + "ChoiceDelta", + "ChoiceDeltaToolCall", + "ChoiceDeltaToolCallFunction", + "ChoiceLogprobs", + "ChoiceLogprobsContent", + "ChoiceLogprobsContentTopLogprob", + "ChoiceLogprobsRefusal", + "ChoiceLogprobsRefusalTopLogprob", + "ChoiceMessage", + "ChoiceMessageToolCall", + "ChoiceMessageToolCallFunction", + "ChoiceSource", + "ChoiceSubquery", + "ChoiceSubquerySource", + "Usage", + "UsageCompletionTokensDetails", + "UsagePromptTokenDetails", +] + + +class ChoiceDeltaToolCallFunction(BaseModel): + arguments: Optional[str] = None + + name: Optional[str] = None + + +class ChoiceDeltaToolCall(BaseModel): + id: Optional[str] = None + + function: Optional[ChoiceDeltaToolCallFunction] = None + + index: Optional[int] = None + + type: Optional[str] = None + + +class ChoiceDelta(BaseModel): + role: Literal["user", "assistant", "system"] + """ + Specifies the role associated with the content, indicating whether the message + is from the 'assistant' or another defined role, helping to contextualize the + output within the interaction flow. + """ + + content: Optional[str] = None + """The text content produced by the model. + + This field contains the actual output generated, reflecting the model's response + to the input query or command. + """ + + tool_calls: Optional[List[ChoiceDeltaToolCall]] = None + + +class ChoiceLogprobsContentTopLogprob(BaseModel): + token: str + + logprob: float + + bytes: Optional[List[int]] = None + + +class ChoiceLogprobsContent(BaseModel): + token: str + + logprob: float + + top_logprobs: List[ChoiceLogprobsContentTopLogprob] + + bytes: Optional[List[int]] = None + + +class ChoiceLogprobsRefusalTopLogprob(BaseModel): + token: str + + logprob: float + + bytes: Optional[List[int]] = None + + +class ChoiceLogprobsRefusal(BaseModel): + token: str + + logprob: float + + top_logprobs: List[ChoiceLogprobsRefusalTopLogprob] + + bytes: Optional[List[int]] = None + + +class ChoiceLogprobs(BaseModel): + content: Optional[List[ChoiceLogprobsContent]] = None + + refusal: Optional[List[ChoiceLogprobsRefusal]] = None + + +class ChoiceMessageToolCallFunction(BaseModel): + arguments: Optional[str] = None + + name: Optional[str] = None + + +class ChoiceMessageToolCall(BaseModel): + id: Optional[str] = None + + function: Optional[ChoiceMessageToolCallFunction] = None + + index: Optional[int] = None + + type: Optional[str] = None + + +class ChoiceMessage(BaseModel): + role: Literal["user", "assistant", "system"] + """ + Specifies the role associated with the content, indicating whether the message + is from the 'assistant' or another defined role, helping to contextualize the + output within the interaction flow. + """ + + content: Optional[str] = None + """The text content produced by the model. + + This field contains the actual output generated, reflecting the model's response + to the input query or command. + """ + + tool_calls: Optional[List[ChoiceMessageToolCall]] = None + + +class ChoiceSource(BaseModel): + file_id: str + """The unique identifier of the file.""" + + snippet: str + """A snippet of text from the source file.""" + + +class ChoiceSubquerySource(BaseModel): + file_id: str + """The unique identifier of the file.""" + + snippet: str + """A snippet of text from the source file.""" + + +class ChoiceSubquery(BaseModel): + answer: str + """The answer to the subquery.""" + + query: str + """The subquery that was asked.""" + + sources: List[ChoiceSubquerySource] + + +class Choice(BaseModel): + delta: ChoiceDelta + """A chat completion delta generated by streamed model responses.""" + + index: int + """The index of the choice in the list of completions generated by the model.""" + + finish_reason: Optional[Literal["stop", "length", "content_filter", "tool_calls"]] = None + """Describes the condition under which the model ceased generating content. + + Common reasons include 'length' (reached the maximum output size), 'stop' + (encountered a stop sequence), 'content_filter' (harmful content filtered out), + or 'tool_calls' (encountered tool calls). + """ + + logprobs: Optional[ChoiceLogprobs] = None + """Log probability information for the choice.""" + + message: Optional[ChoiceMessage] = None + """The chat completion message from the model. + + Note: this field is deprecated for streaming. Use `delta` instead. + """ + + sources: Optional[List[ChoiceSource]] = None + """An array of source objects that provide context for the model's response. + + Only returned when using the Knowledge Graph chat tool. + """ + + subqueries: Optional[List[ChoiceSubquery]] = None + """An array of sub-query objects that provide context for the model's response. + + Only returned when using the Knowledge Graph chat tool. + """ + + +class UsageCompletionTokensDetails(BaseModel): + reasoning_tokens: int + + +class UsagePromptTokenDetails(BaseModel): + cached_tokens: int + + +class Usage(BaseModel): + completion_tokens: int + + prompt_tokens: int + + total_tokens: int + + completion_tokens_details: Optional[UsageCompletionTokensDetails] = None + + prompt_token_details: Optional[UsagePromptTokenDetails] = None + + +class ChatCompletionChunk(BaseModel): + id: str + """A globally unique identifier (UUID) for the response generated by the API. + + This ID can be used to reference the specific operation or transaction within + the system for tracking or debugging purposes. + """ + + choices: List[Choice] + """ + An array of objects representing the different outcomes or results produced by + the model based on the input provided. + """ + + created: int + """The Unix timestamp (in seconds) when the response was created. + + This timestamp can be used to verify the timing of the response relative to + other events or operations. + """ + + model: str + """Identifies the specific model used to generate the response.""" + + service_tier: Optional[str] = None + + system_fingerprint: Optional[str] = None + + usage: Optional[Usage] = None + """Usage information for the chat completion response. + + Please note that at this time Knowledge Graph tool usage is not included in this + object. + """ From dfa275ad4696a9b6c17d33946d72daf2bf2a94a9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 3 Oct 2024 23:07:18 +0000 Subject: [PATCH 085/399] docs: add pagination example (#82) --- README.md | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/README.md b/README.md index cefba0f8..884b46ac 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,69 @@ Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typ Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`. +## Pagination + +List methods in the Writer API are paginated. + +This library provides auto-paginating iterators with each list response, so you do not have to request successive pages manually: + +```python +from writerai import Writer + +client = Writer() + +all_graphs = [] +# Automatically fetches more pages as needed. +for graph in client.graphs.list(): + # Do something with graph here + all_graphs.append(graph) +print(all_graphs) +``` + +Or, asynchronously: + +```python +import asyncio +from writerai import AsyncWriter + +client = AsyncWriter() + + +async def main() -> None: + all_graphs = [] + # Iterate through items across all pages, issuing requests as needed. + async for graph in client.graphs.list(): + all_graphs.append(graph) + print(all_graphs) + + +asyncio.run(main()) +``` + +Alternatively, you can use the `.has_next_page()`, `.next_page_info()`, or `.get_next_page()` methods for more granular control working with pages: + +```python +first_page = await client.graphs.list() +if first_page.has_next_page(): + print(f"will fetch next page using these details: {first_page.next_page_info()}") + next_page = await first_page.get_next_page() + print(f"number of items we just fetched: {len(next_page.data)}") + +# Remove `await` for non-async usage. +``` + +Or just work directly with the returned data: + +```python +first_page = await client.graphs.list() + +print(f"next page cursor: {first_page.after}") # => "next page cursor: ..." +for graph in first_page.data: + print(graph.id) + +# Remove `await` for non-async usage. +``` + ## Handling errors When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `writerai.APIConnectionError` is raised. From fdb4c59242fec665e17aece679c10f13ec6ec2a7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 4 Oct 2024 18:39:21 +0000 Subject: [PATCH 086/399] docs(api): updates to API spec (#83) --- .stats.yml | 2 +- src/writerai/types/chat.py | 113 ++++++------ src/writerai/types/chat_completion_chunk.py | 179 ++++++++++++-------- 3 files changed, 168 insertions(+), 126 deletions(-) diff --git a/.stats.yml b/.stats.yml index 62289476..13f6f3d6 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-4a11c63c7cb5d0d6e0496e2d0b1094b1b33c6fc3d28cd472547c9b745a7c57ac.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-2ad4cffb18a01119bca51bc468740407407dcf2aea29a0323e3c84662817b000.yml diff --git a/src/writerai/types/chat.py b/src/writerai/types/chat.py index b7c4c001..dae86096 100644 --- a/src/writerai/types/chat.py +++ b/src/writerai/types/chat.py @@ -14,11 +14,12 @@ "ChoiceLogprobsRefusal", "ChoiceLogprobsRefusalTopLogprob", "ChoiceMessage", + "ChoiceMessageGraphData", + "ChoiceMessageGraphDataSource", + "ChoiceMessageGraphDataSubquery", + "ChoiceMessageGraphDataSubquerySource", "ChoiceMessageToolCall", "ChoiceMessageToolCallFunction", - "ChoiceSource", - "ChoiceSubquery", - "ChoiceSubquerySource", "Usage", "UsageCompletionTokensDetails", "UsagePromptTokenDetails", @@ -67,71 +68,76 @@ class ChoiceLogprobs(BaseModel): refusal: Optional[List[ChoiceLogprobsRefusal]] = None -class ChoiceMessageToolCallFunction(BaseModel): - arguments: Optional[str] = None +class ChoiceMessageGraphDataSource(BaseModel): + file_id: str + """The unique identifier of the file.""" - name: Optional[str] = None + snippet: str + """A snippet of text from the source file.""" -class ChoiceMessageToolCall(BaseModel): - id: Optional[str] = None +class ChoiceMessageGraphDataSubquerySource(BaseModel): + file_id: str + """The unique identifier of the file.""" - function: Optional[ChoiceMessageToolCallFunction] = None + snippet: str + """A snippet of text from the source file.""" - index: Optional[int] = None - type: Optional[str] = None +class ChoiceMessageGraphDataSubquery(BaseModel): + answer: str + """The answer to the subquery.""" + query: str + """The subquery that was asked.""" -class ChoiceMessage(BaseModel): - role: Literal["user", "assistant", "system"] - """ - Specifies the role associated with the content, indicating whether the message - is from the 'assistant' or another defined role, helping to contextualize the - output within the interaction flow. - """ + sources: List[ChoiceMessageGraphDataSubquerySource] - content: Optional[str] = None - """The text content produced by the model. - This field contains the actual output generated, reflecting the model's response - to the input query or command. - """ +class ChoiceMessageGraphData(BaseModel): + sources: Optional[List[ChoiceMessageGraphDataSource]] = None - tool_calls: Optional[List[ChoiceMessageToolCall]] = None + status: Optional[Literal["processing", "finished"]] = None + subqueries: Optional[List[ChoiceMessageGraphDataSubquery]] = None -class ChoiceSource(BaseModel): - file_id: str - """The unique identifier of the file.""" - snippet: str - """A snippet of text from the source file.""" +class ChoiceMessageToolCallFunction(BaseModel): + arguments: str + name: str -class ChoiceSubquerySource(BaseModel): - file_id: str - """The unique identifier of the file.""" - snippet: str - """A snippet of text from the source file.""" +class ChoiceMessageToolCall(BaseModel): + id: str + function: ChoiceMessageToolCallFunction -class ChoiceSubquery(BaseModel): - answer: str - """The answer to the subquery.""" + type: str + + index: Optional[int] = None - query: str - """The subquery that was asked.""" - sources: List[ChoiceSubquerySource] +class ChoiceMessage(BaseModel): + content: str + """The text content produced by the model. + This field contains the actual output generated, reflecting the model's response + to the input query or command. + """ -class Choice(BaseModel): - index: int - """The index of the choice in the list of completions generated by the model.""" + refusal: str - finish_reason: Optional[Literal["stop", "length", "content_filter", "tool_calls"]] = None + role: Literal["assistant"] + """Specifies the role associated with the content.""" + + graph_data: Optional[ChoiceMessageGraphData] = None + + tool_calls: Optional[List[ChoiceMessageToolCall]] = None + + +class Choice(BaseModel): + finish_reason: Literal["stop", "length", "content_filter", "tool_calls"] """Describes the condition under which the model ceased generating content. Common reasons include 'length' (reached the maximum output size), 'stop' @@ -139,27 +145,18 @@ class Choice(BaseModel): or 'tool_calls' (encountered tool calls). """ - logprobs: Optional[ChoiceLogprobs] = None + index: int + """The index of the choice in the list of completions generated by the model.""" + + logprobs: ChoiceLogprobs """Log probability information for the choice.""" - message: Optional[ChoiceMessage] = None + message: ChoiceMessage """The chat completion message from the model. Note: this field is deprecated for streaming. Use `delta` instead. """ - sources: Optional[List[ChoiceSource]] = None - """An array of source objects that provide context for the model's response. - - Only returned when using the Knowledge Graph chat tool. - """ - - subqueries: Optional[List[ChoiceSubquery]] = None - """An array of sub-query objects that provide context for the model's response. - - Only returned when using the Knowledge Graph chat tool. - """ - class UsageCompletionTokensDetails(BaseModel): reasoning_tokens: int diff --git a/src/writerai/types/chat_completion_chunk.py b/src/writerai/types/chat_completion_chunk.py index c48667cd..969b67a1 100644 --- a/src/writerai/types/chat_completion_chunk.py +++ b/src/writerai/types/chat_completion_chunk.py @@ -9,6 +9,10 @@ "ChatCompletionChunk", "Choice", "ChoiceDelta", + "ChoiceDeltaGraphData", + "ChoiceDeltaGraphDataSource", + "ChoiceDeltaGraphDataSubquery", + "ChoiceDeltaGraphDataSubquerySource", "ChoiceDeltaToolCall", "ChoiceDeltaToolCallFunction", "ChoiceLogprobs", @@ -17,41 +21,69 @@ "ChoiceLogprobsRefusal", "ChoiceLogprobsRefusalTopLogprob", "ChoiceMessage", + "ChoiceMessageGraphData", + "ChoiceMessageGraphDataSource", + "ChoiceMessageGraphDataSubquery", + "ChoiceMessageGraphDataSubquerySource", "ChoiceMessageToolCall", "ChoiceMessageToolCallFunction", - "ChoiceSource", - "ChoiceSubquery", - "ChoiceSubquerySource", "Usage", "UsageCompletionTokensDetails", "UsagePromptTokenDetails", ] +class ChoiceDeltaGraphDataSource(BaseModel): + file_id: str + """The unique identifier of the file.""" + + snippet: str + """A snippet of text from the source file.""" + + +class ChoiceDeltaGraphDataSubquerySource(BaseModel): + file_id: str + """The unique identifier of the file.""" + + snippet: str + """A snippet of text from the source file.""" + + +class ChoiceDeltaGraphDataSubquery(BaseModel): + answer: str + """The answer to the subquery.""" + + query: str + """The subquery that was asked.""" + + sources: List[ChoiceDeltaGraphDataSubquerySource] + + +class ChoiceDeltaGraphData(BaseModel): + sources: Optional[List[ChoiceDeltaGraphDataSource]] = None + + status: Optional[Literal["processing", "finished"]] = None + + subqueries: Optional[List[ChoiceDeltaGraphDataSubquery]] = None + + class ChoiceDeltaToolCallFunction(BaseModel): - arguments: Optional[str] = None + arguments: str - name: Optional[str] = None + name: str class ChoiceDeltaToolCall(BaseModel): + index: int + id: Optional[str] = None function: Optional[ChoiceDeltaToolCallFunction] = None - index: Optional[int] = None - type: Optional[str] = None class ChoiceDelta(BaseModel): - role: Literal["user", "assistant", "system"] - """ - Specifies the role associated with the content, indicating whether the message - is from the 'assistant' or another defined role, helping to contextualize the - output within the interaction flow. - """ - content: Optional[str] = None """The text content produced by the model. @@ -59,6 +91,17 @@ class ChoiceDelta(BaseModel): to the input query or command. """ + graph_data: Optional[ChoiceDeltaGraphData] = None + + refusal: Optional[str] = None + + role: Optional[Literal["user", "assistant", "system"]] = None + """ + Specifies the role associated with the content, indicating whether the message + is from the 'assistant' or another defined role, helping to contextualize the + output within the interaction flow. + """ + tool_calls: Optional[List[ChoiceDeltaToolCall]] = None @@ -104,74 +147,79 @@ class ChoiceLogprobs(BaseModel): refusal: Optional[List[ChoiceLogprobsRefusal]] = None -class ChoiceMessageToolCallFunction(BaseModel): - arguments: Optional[str] = None +class ChoiceMessageGraphDataSource(BaseModel): + file_id: str + """The unique identifier of the file.""" - name: Optional[str] = None + snippet: str + """A snippet of text from the source file.""" -class ChoiceMessageToolCall(BaseModel): - id: Optional[str] = None +class ChoiceMessageGraphDataSubquerySource(BaseModel): + file_id: str + """The unique identifier of the file.""" - function: Optional[ChoiceMessageToolCallFunction] = None + snippet: str + """A snippet of text from the source file.""" - index: Optional[int] = None - type: Optional[str] = None +class ChoiceMessageGraphDataSubquery(BaseModel): + answer: str + """The answer to the subquery.""" + query: str + """The subquery that was asked.""" -class ChoiceMessage(BaseModel): - role: Literal["user", "assistant", "system"] - """ - Specifies the role associated with the content, indicating whether the message - is from the 'assistant' or another defined role, helping to contextualize the - output within the interaction flow. - """ + sources: List[ChoiceMessageGraphDataSubquerySource] - content: Optional[str] = None - """The text content produced by the model. - This field contains the actual output generated, reflecting the model's response - to the input query or command. - """ +class ChoiceMessageGraphData(BaseModel): + sources: Optional[List[ChoiceMessageGraphDataSource]] = None - tool_calls: Optional[List[ChoiceMessageToolCall]] = None + status: Optional[Literal["processing", "finished"]] = None + subqueries: Optional[List[ChoiceMessageGraphDataSubquery]] = None -class ChoiceSource(BaseModel): - file_id: str - """The unique identifier of the file.""" - snippet: str - """A snippet of text from the source file.""" +class ChoiceMessageToolCallFunction(BaseModel): + arguments: str + name: str -class ChoiceSubquerySource(BaseModel): - file_id: str - """The unique identifier of the file.""" - snippet: str - """A snippet of text from the source file.""" +class ChoiceMessageToolCall(BaseModel): + id: str + function: ChoiceMessageToolCallFunction -class ChoiceSubquery(BaseModel): - answer: str - """The answer to the subquery.""" + type: str - query: str - """The subquery that was asked.""" + index: Optional[int] = None - sources: List[ChoiceSubquerySource] + +class ChoiceMessage(BaseModel): + content: str + """The text content produced by the model. + + This field contains the actual output generated, reflecting the model's response + to the input query or command. + """ + + refusal: str + + role: Literal["assistant"] + """Specifies the role associated with the content.""" + + graph_data: Optional[ChoiceMessageGraphData] = None + + tool_calls: Optional[List[ChoiceMessageToolCall]] = None class Choice(BaseModel): delta: ChoiceDelta """A chat completion delta generated by streamed model responses.""" - index: int - """The index of the choice in the list of completions generated by the model.""" - - finish_reason: Optional[Literal["stop", "length", "content_filter", "tool_calls"]] = None + finish_reason: Literal["stop", "length", "content_filter", "tool_calls"] """Describes the condition under which the model ceased generating content. Common reasons include 'length' (reached the maximum output size), 'stop' @@ -179,6 +227,9 @@ class Choice(BaseModel): or 'tool_calls' (encountered tool calls). """ + index: int + """The index of the choice in the list of completions generated by the model.""" + logprobs: Optional[ChoiceLogprobs] = None """Log probability information for the choice.""" @@ -188,18 +239,6 @@ class Choice(BaseModel): Note: this field is deprecated for streaming. Use `delta` instead. """ - sources: Optional[List[ChoiceSource]] = None - """An array of source objects that provide context for the model's response. - - Only returned when using the Knowledge Graph chat tool. - """ - - subqueries: Optional[List[ChoiceSubquery]] = None - """An array of sub-query objects that provide context for the model's response. - - Only returned when using the Knowledge Graph chat tool. - """ - class UsageCompletionTokensDetails(BaseModel): reasoning_tokens: int @@ -245,6 +284,12 @@ class ChatCompletionChunk(BaseModel): model: str """Identifies the specific model used to generate the response.""" + object: str + """ + The type of object returned, which is always `chat.completion.chunk` for + streaming chat responses. + """ + service_tier: Optional[str] = None system_fingerprint: Optional[str] = None From b90df2b62f81f81590cbd7e43e659eb1ffa9615c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 4 Oct 2024 18:40:22 +0000 Subject: [PATCH 087/399] chore(internal): add support for parsing bool response content (#84) --- src/writerai/_response.py | 3 +++ tests/test_response.py | 50 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/src/writerai/_response.py b/src/writerai/_response.py index f6917b19..3a1356d7 100644 --- a/src/writerai/_response.py +++ b/src/writerai/_response.py @@ -192,6 +192,9 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: if cast_to == float: return cast(R, float(response.text)) + if cast_to == bool: + return cast(R, response.text.lower() == "true") + origin = get_origin(cast_to) or cast_to if origin == APIResponse: diff --git a/tests/test_response.py b/tests/test_response.py index 1a496847..dcbaece2 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -190,6 +190,56 @@ async def test_async_response_parse_annotated_type(async_client: AsyncWriter) -> assert obj.bar == 2 +@pytest.mark.parametrize( + "content, expected", + [ + ("false", False), + ("true", True), + ("False", False), + ("True", True), + ("TrUe", True), + ("FalSe", False), + ], +) +def test_response_parse_bool(client: Writer, content: str, expected: bool) -> None: + response = APIResponse( + raw=httpx.Response(200, content=content), + client=client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + result = response.parse(to=bool) + assert result is expected + + +@pytest.mark.parametrize( + "content, expected", + [ + ("false", False), + ("true", True), + ("False", False), + ("True", True), + ("TrUe", True), + ("FalSe", False), + ], +) +async def test_async_response_parse_bool(client: AsyncWriter, content: str, expected: bool) -> None: + response = AsyncAPIResponse( + raw=httpx.Response(200, content=content), + client=client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + result = await response.parse(to=bool) + assert result is expected + + class OtherModel(BaseModel): a: str From 6a33b7341a998e6413fab759c73b776629e247b9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 8 Oct 2024 21:20:07 +0000 Subject: [PATCH 088/399] docs(api): updates to API spec (#86) --- .stats.yml | 2 +- README.md | 45 ++--------- src/writerai/types/chat_chat_params.py | 6 +- tests/api_resources/test_chat.py | 96 +++++----------------- tests/test_client.py | 108 +++---------------------- 5 files changed, 42 insertions(+), 215 deletions(-) diff --git a/.stats.yml b/.stats.yml index 13f6f3d6..be523581 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-2ad4cffb18a01119bca51bc468740407407dcf2aea29a0323e3c84662817b000.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-330636638e8423bfe0b00d5b5a5f605f9a64c59f27a3a45dd4c7ff88b9411c36.yml diff --git a/README.md b/README.md index 884b46ac..00deedc2 100644 --- a/README.md +++ b/README.md @@ -33,12 +33,7 @@ client = Writer( ) chat = client.chat.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-002-32k", ) print(chat.id) @@ -66,12 +61,7 @@ client = AsyncWriter( async def main() -> None: chat = await client.chat.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-002-32k", ) print(chat.id) @@ -205,12 +195,7 @@ client = Writer() try: client.chat.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-002-32k", ) except writerai.APIConnectionError as e: @@ -256,12 +241,7 @@ client = Writer( # Or, configure per-request: client.with_options(max_retries=5).chat.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-002-32k", ) ``` @@ -287,12 +267,7 @@ client = Writer( # Override per-request: client.with_options(timeout=5.0).chat.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-002-32k", ) ``` @@ -335,8 +310,7 @@ from writerai import Writer client = Writer() response = client.chat.with_raw_response.chat( messages=[{ - "content": "Write a memo summarizing this earnings report.", - "role": "user", + "role": "user" }], model="palmyra-x-002-32k", ) @@ -358,12 +332,7 @@ To stream the response body, use `.with_streaming_response` instead, which requi ```python with client.chat.with_streaming_response.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-002-32k", ) as response: print(response.headers.get("X-My-Header")) diff --git a/src/writerai/types/chat_chat_params.py b/src/writerai/types/chat_chat_params.py index 40ac0a13..3e3de6be 100644 --- a/src/writerai/types/chat_chat_params.py +++ b/src/writerai/types/chat_chat_params.py @@ -94,12 +94,14 @@ class ChatChatParamsBase(TypedDict, total=False): class Message(TypedDict, total=False): - content: Required[str] + role: Required[Literal["user", "assistant", "system", "tool"]] - role: Required[Literal["user", "assistant", "system"]] + content: str name: str + tool_id: str + class StreamOptions(TypedDict, total=False): include_usage: Required[bool] diff --git a/tests/api_resources/test_chat.py b/tests/api_resources/test_chat.py index 2bbe527d..c5d36794 100644 --- a/tests/api_resources/test_chat.py +++ b/tests/api_resources/test_chat.py @@ -20,12 +20,7 @@ class TestChat: @parametrize def test_method_chat_overload_1(self, client: Writer) -> None: chat = client.chat.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-004", ) assert_matches_type(Chat, chat, path=["response"]) @@ -35,9 +30,10 @@ def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: chat = client.chat.chat( messages=[ { - "content": "Write a memo summarizing this earnings report.", "role": "user", + "content": "Write a memo summarizing this earnings report.", "name": "name", + "tool_id": "tool_id", } ], model="palmyra-x-004", @@ -79,12 +75,7 @@ def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: @parametrize def test_raw_response_chat_overload_1(self, client: Writer) -> None: response = client.chat.with_raw_response.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-004", ) @@ -96,12 +87,7 @@ def test_raw_response_chat_overload_1(self, client: Writer) -> None: @parametrize def test_streaming_response_chat_overload_1(self, client: Writer) -> None: with client.chat.with_streaming_response.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-004", ) as response: assert not response.is_closed @@ -115,12 +101,7 @@ def test_streaming_response_chat_overload_1(self, client: Writer) -> None: @parametrize def test_method_chat_overload_2(self, client: Writer) -> None: chat_stream = client.chat.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-004", stream=True, ) @@ -131,9 +112,10 @@ def test_method_chat_with_all_params_overload_2(self, client: Writer) -> None: chat_stream = client.chat.chat( messages=[ { - "content": "Write a memo summarizing this earnings report.", "role": "user", + "content": "Write a memo summarizing this earnings report.", "name": "name", + "tool_id": "tool_id", } ], model="palmyra-x-004", @@ -175,12 +157,7 @@ def test_method_chat_with_all_params_overload_2(self, client: Writer) -> None: @parametrize def test_raw_response_chat_overload_2(self, client: Writer) -> None: response = client.chat.with_raw_response.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-004", stream=True, ) @@ -192,12 +169,7 @@ def test_raw_response_chat_overload_2(self, client: Writer) -> None: @parametrize def test_streaming_response_chat_overload_2(self, client: Writer) -> None: with client.chat.with_streaming_response.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-004", stream=True, ) as response: @@ -216,12 +188,7 @@ class TestAsyncChat: @parametrize async def test_method_chat_overload_1(self, async_client: AsyncWriter) -> None: chat = await async_client.chat.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-004", ) assert_matches_type(Chat, chat, path=["response"]) @@ -231,9 +198,10 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW chat = await async_client.chat.chat( messages=[ { - "content": "Write a memo summarizing this earnings report.", "role": "user", + "content": "Write a memo summarizing this earnings report.", "name": "name", + "tool_id": "tool_id", } ], model="palmyra-x-004", @@ -275,12 +243,7 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW @parametrize async def test_raw_response_chat_overload_1(self, async_client: AsyncWriter) -> None: response = await async_client.chat.with_raw_response.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-004", ) @@ -292,12 +255,7 @@ async def test_raw_response_chat_overload_1(self, async_client: AsyncWriter) -> @parametrize async def test_streaming_response_chat_overload_1(self, async_client: AsyncWriter) -> None: async with async_client.chat.with_streaming_response.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-004", ) as response: assert not response.is_closed @@ -311,12 +269,7 @@ async def test_streaming_response_chat_overload_1(self, async_client: AsyncWrite @parametrize async def test_method_chat_overload_2(self, async_client: AsyncWriter) -> None: chat_stream = await async_client.chat.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-004", stream=True, ) @@ -327,9 +280,10 @@ async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncW chat_stream = await async_client.chat.chat( messages=[ { - "content": "Write a memo summarizing this earnings report.", "role": "user", + "content": "Write a memo summarizing this earnings report.", "name": "name", + "tool_id": "tool_id", } ], model="palmyra-x-004", @@ -371,12 +325,7 @@ async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncW @parametrize async def test_raw_response_chat_overload_2(self, async_client: AsyncWriter) -> None: response = await async_client.chat.with_raw_response.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-004", stream=True, ) @@ -388,12 +337,7 @@ async def test_raw_response_chat_overload_2(self, async_client: AsyncWriter) -> @parametrize async def test_streaming_response_chat_overload_2(self, async_client: AsyncWriter) -> None: async with async_client.chat.with_streaming_response.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-004", stream=True, ) as response: diff --git a/tests/test_client.py b/tests/test_client.py index 3c9c1cef..a6a96b43 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -721,18 +721,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> No with pytest.raises(APITimeoutError): self.client.post( "/v1/chat", - body=cast( - object, - dict( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], - model="palmyra-x-002-32k", - ), - ), + body=cast(object, dict(messages=[{"role": "user"}], model="palmyra-x-002-32k")), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -747,18 +736,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> Non with pytest.raises(APIStatusError): self.client.post( "/v1/chat", - body=cast( - object, - dict( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], - model="palmyra-x-002-32k", - ), - ), + body=cast(object, dict(messages=[{"role": "user"}], model="palmyra-x-002-32k")), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -782,15 +760,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) - response = client.chat.with_raw_response.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], - model="palmyra-x-004", - ) + response = client.chat.with_raw_response.chat(messages=[{"role": "user"}], model="palmyra-x-004") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -815,14 +785,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) response = client.chat.with_raw_response.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], - model="palmyra-x-004", - extra_headers={"x-stainless-retry-count": Omit()}, + messages=[{"role": "user"}], model="palmyra-x-004", extra_headers={"x-stainless-retry-count": Omit()} ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -847,14 +810,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) response = client.chat.with_raw_response.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], - model="palmyra-x-004", - extra_headers={"x-stainless-retry-count": "42"}, + messages=[{"role": "user"}], model="palmyra-x-004", extra_headers={"x-stainless-retry-count": "42"} ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" @@ -1547,18 +1503,7 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) with pytest.raises(APITimeoutError): await self.client.post( "/v1/chat", - body=cast( - object, - dict( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], - model="palmyra-x-002-32k", - ), - ), + body=cast(object, dict(messages=[{"role": "user"}], model="palmyra-x-002-32k")), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -1573,18 +1518,7 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) with pytest.raises(APIStatusError): await self.client.post( "/v1/chat", - body=cast( - object, - dict( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], - model="palmyra-x-002-32k", - ), - ), + body=cast(object, dict(messages=[{"role": "user"}], model="palmyra-x-002-32k")), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -1611,15 +1545,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) - response = await client.chat.with_raw_response.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], - model="palmyra-x-004", - ) + response = await client.chat.with_raw_response.chat(messages=[{"role": "user"}], model="palmyra-x-004") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -1645,14 +1571,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) response = await client.chat.with_raw_response.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], - model="palmyra-x-004", - extra_headers={"x-stainless-retry-count": Omit()}, + messages=[{"role": "user"}], model="palmyra-x-004", extra_headers={"x-stainless-retry-count": Omit()} ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -1678,14 +1597,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) response = await client.chat.with_raw_response.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], - model="palmyra-x-004", - extra_headers={"x-stainless-retry-count": "42"}, + messages=[{"role": "user"}], model="palmyra-x-004", extra_headers={"x-stainless-retry-count": "42"} ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" From 9ec48bfa45479c3aad049a763b39955cb8697489 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 8 Oct 2024 21:21:43 +0000 Subject: [PATCH 089/399] fix(client): avoid OverflowError with very large retry counts (#87) --- src/writerai/_base_client.py | 3 ++- tests/test_client.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index bf35ba1f..5e2a0be2 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -689,7 +689,8 @@ def _calculate_retry_timeout( if retry_after is not None and 0 < retry_after <= 60: return retry_after - nb_retries = max_retries - remaining_retries + # Also cap retry count to 1000 to avoid any potential overflows with `pow` + nb_retries = min(max_retries - remaining_retries, 1000) # Apply exponential backoff, but not more than the max. sleep_seconds = min(INITIAL_RETRY_DELAY * pow(2.0, nb_retries), MAX_RETRY_DELAY) diff --git a/tests/test_client.py b/tests/test_client.py index a6a96b43..09eb207e 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -702,6 +702,7 @@ class Model(BaseModel): [3, "", 0.5], [2, "", 0.5 * 2.0], [1, "", 0.5 * 4.0], + [-1100, "", 7.8], # test large number potentially overflowing ], ) @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) @@ -1483,6 +1484,7 @@ class Model(BaseModel): [3, "", 0.5], [2, "", 0.5 * 2.0], [1, "", 0.5 * 4.0], + [-1100, "", 7.8], # test large number potentially overflowing ], ) @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) From 53fa40436ca99dea127ab735cdc674ff8a0cb0bf Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 8 Oct 2024 21:22:32 +0000 Subject: [PATCH 090/399] chore: add repr to PageInfo class (#88) --- src/writerai/_base_client.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index 5e2a0be2..9c96f633 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -143,6 +143,12 @@ def __init__( self.url = url self.params = params + @override + def __repr__(self) -> str: + if self.url: + return f"{self.__class__.__name__}(url={self.url})" + return f"{self.__class__.__name__}(params={self.params})" + class BasePage(GenericModel, Generic[_T]): """ From af241dc3ef774cc4c8ce4c93ae765831a191616b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 8 Oct 2024 21:36:17 +0000 Subject: [PATCH 091/399] feat(api): update models in readme (#89) --- .stats.yml | 2 +- README.md | 63 +++++++++++---- src/writerai/types/chat_chat_params.py | 6 +- tests/api_resources/test_chat.py | 96 +++++++++++++++++----- tests/test_client.py | 108 ++++++++++++++++++++++--- 5 files changed, 224 insertions(+), 51 deletions(-) diff --git a/.stats.yml b/.stats.yml index be523581..13f6f3d6 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-330636638e8423bfe0b00d5b5a5f605f9a64c59f27a3a45dd4c7ff88b9411c36.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-2ad4cffb18a01119bca51bc468740407407dcf2aea29a0323e3c84662817b000.yml diff --git a/README.md b/README.md index 00deedc2..e16571cf 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,13 @@ client = Writer( ) chat = client.chat.chat( - messages=[{"role": "user"}], - model="palmyra-x-002-32k", + messages=[ + { + "content": "Write a memo summarizing this earnings report.", + "role": "user", + } + ], + model="palmyra-x-004", ) print(chat.id) ``` @@ -61,8 +66,13 @@ client = AsyncWriter( async def main() -> None: chat = await client.chat.chat( - messages=[{"role": "user"}], - model="palmyra-x-002-32k", + messages=[ + { + "content": "Write a memo summarizing this earnings report.", + "role": "user", + } + ], + model="palmyra-x-004", ) print(chat.id) @@ -82,7 +92,7 @@ from writerai import Writer client = Writer() stream = client.completions.create( - model="palmyra-x-002-instruct", + model="palmyra-x-003-instruct", prompt="Hi, my name is", stream=True, ) @@ -98,7 +108,7 @@ from writerai import AsyncWriter client = AsyncWriter() stream = await client.completions.create( - model="palmyra-x-002-instruct", + model="palmyra-x-003-instruct", prompt="Hi, my name is", stream=True, ) @@ -195,8 +205,13 @@ client = Writer() try: client.chat.chat( - messages=[{"role": "user"}], - model="palmyra-x-002-32k", + messages=[ + { + "content": "Write a memo summarizing this earnings report.", + "role": "user", + } + ], + model="palmyra-x-004", ) except writerai.APIConnectionError as e: print("The server could not be reached") @@ -241,8 +256,13 @@ client = Writer( # Or, configure per-request: client.with_options(max_retries=5).chat.chat( - messages=[{"role": "user"}], - model="palmyra-x-002-32k", + messages=[ + { + "content": "Write a memo summarizing this earnings report.", + "role": "user", + } + ], + model="palmyra-x-004", ) ``` @@ -267,8 +287,13 @@ client = Writer( # Override per-request: client.with_options(timeout=5.0).chat.chat( - messages=[{"role": "user"}], - model="palmyra-x-002-32k", + messages=[ + { + "content": "Write a memo summarizing this earnings report.", + "role": "user", + } + ], + model="palmyra-x-004", ) ``` @@ -310,9 +335,10 @@ from writerai import Writer client = Writer() response = client.chat.with_raw_response.chat( messages=[{ - "role": "user" + "content": "Write a memo summarizing this earnings report.", + "role": "user", }], - model="palmyra-x-002-32k", + model="palmyra-x-004", ) print(response.headers.get('X-My-Header')) @@ -332,8 +358,13 @@ To stream the response body, use `.with_streaming_response` instead, which requi ```python with client.chat.with_streaming_response.chat( - messages=[{"role": "user"}], - model="palmyra-x-002-32k", + messages=[ + { + "content": "Write a memo summarizing this earnings report.", + "role": "user", + } + ], + model="palmyra-x-004", ) as response: print(response.headers.get("X-My-Header")) diff --git a/src/writerai/types/chat_chat_params.py b/src/writerai/types/chat_chat_params.py index 3e3de6be..40ac0a13 100644 --- a/src/writerai/types/chat_chat_params.py +++ b/src/writerai/types/chat_chat_params.py @@ -94,14 +94,12 @@ class ChatChatParamsBase(TypedDict, total=False): class Message(TypedDict, total=False): - role: Required[Literal["user", "assistant", "system", "tool"]] + content: Required[str] - content: str + role: Required[Literal["user", "assistant", "system"]] name: str - tool_id: str - class StreamOptions(TypedDict, total=False): include_usage: Required[bool] diff --git a/tests/api_resources/test_chat.py b/tests/api_resources/test_chat.py index c5d36794..2bbe527d 100644 --- a/tests/api_resources/test_chat.py +++ b/tests/api_resources/test_chat.py @@ -20,7 +20,12 @@ class TestChat: @parametrize def test_method_chat_overload_1(self, client: Writer) -> None: chat = client.chat.chat( - messages=[{"role": "user"}], + messages=[ + { + "content": "Write a memo summarizing this earnings report.", + "role": "user", + } + ], model="palmyra-x-004", ) assert_matches_type(Chat, chat, path=["response"]) @@ -30,10 +35,9 @@ def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: chat = client.chat.chat( messages=[ { - "role": "user", "content": "Write a memo summarizing this earnings report.", + "role": "user", "name": "name", - "tool_id": "tool_id", } ], model="palmyra-x-004", @@ -75,7 +79,12 @@ def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: @parametrize def test_raw_response_chat_overload_1(self, client: Writer) -> None: response = client.chat.with_raw_response.chat( - messages=[{"role": "user"}], + messages=[ + { + "content": "Write a memo summarizing this earnings report.", + "role": "user", + } + ], model="palmyra-x-004", ) @@ -87,7 +96,12 @@ def test_raw_response_chat_overload_1(self, client: Writer) -> None: @parametrize def test_streaming_response_chat_overload_1(self, client: Writer) -> None: with client.chat.with_streaming_response.chat( - messages=[{"role": "user"}], + messages=[ + { + "content": "Write a memo summarizing this earnings report.", + "role": "user", + } + ], model="palmyra-x-004", ) as response: assert not response.is_closed @@ -101,7 +115,12 @@ def test_streaming_response_chat_overload_1(self, client: Writer) -> None: @parametrize def test_method_chat_overload_2(self, client: Writer) -> None: chat_stream = client.chat.chat( - messages=[{"role": "user"}], + messages=[ + { + "content": "Write a memo summarizing this earnings report.", + "role": "user", + } + ], model="palmyra-x-004", stream=True, ) @@ -112,10 +131,9 @@ def test_method_chat_with_all_params_overload_2(self, client: Writer) -> None: chat_stream = client.chat.chat( messages=[ { - "role": "user", "content": "Write a memo summarizing this earnings report.", + "role": "user", "name": "name", - "tool_id": "tool_id", } ], model="palmyra-x-004", @@ -157,7 +175,12 @@ def test_method_chat_with_all_params_overload_2(self, client: Writer) -> None: @parametrize def test_raw_response_chat_overload_2(self, client: Writer) -> None: response = client.chat.with_raw_response.chat( - messages=[{"role": "user"}], + messages=[ + { + "content": "Write a memo summarizing this earnings report.", + "role": "user", + } + ], model="palmyra-x-004", stream=True, ) @@ -169,7 +192,12 @@ def test_raw_response_chat_overload_2(self, client: Writer) -> None: @parametrize def test_streaming_response_chat_overload_2(self, client: Writer) -> None: with client.chat.with_streaming_response.chat( - messages=[{"role": "user"}], + messages=[ + { + "content": "Write a memo summarizing this earnings report.", + "role": "user", + } + ], model="palmyra-x-004", stream=True, ) as response: @@ -188,7 +216,12 @@ class TestAsyncChat: @parametrize async def test_method_chat_overload_1(self, async_client: AsyncWriter) -> None: chat = await async_client.chat.chat( - messages=[{"role": "user"}], + messages=[ + { + "content": "Write a memo summarizing this earnings report.", + "role": "user", + } + ], model="palmyra-x-004", ) assert_matches_type(Chat, chat, path=["response"]) @@ -198,10 +231,9 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW chat = await async_client.chat.chat( messages=[ { - "role": "user", "content": "Write a memo summarizing this earnings report.", + "role": "user", "name": "name", - "tool_id": "tool_id", } ], model="palmyra-x-004", @@ -243,7 +275,12 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW @parametrize async def test_raw_response_chat_overload_1(self, async_client: AsyncWriter) -> None: response = await async_client.chat.with_raw_response.chat( - messages=[{"role": "user"}], + messages=[ + { + "content": "Write a memo summarizing this earnings report.", + "role": "user", + } + ], model="palmyra-x-004", ) @@ -255,7 +292,12 @@ async def test_raw_response_chat_overload_1(self, async_client: AsyncWriter) -> @parametrize async def test_streaming_response_chat_overload_1(self, async_client: AsyncWriter) -> None: async with async_client.chat.with_streaming_response.chat( - messages=[{"role": "user"}], + messages=[ + { + "content": "Write a memo summarizing this earnings report.", + "role": "user", + } + ], model="palmyra-x-004", ) as response: assert not response.is_closed @@ -269,7 +311,12 @@ async def test_streaming_response_chat_overload_1(self, async_client: AsyncWrite @parametrize async def test_method_chat_overload_2(self, async_client: AsyncWriter) -> None: chat_stream = await async_client.chat.chat( - messages=[{"role": "user"}], + messages=[ + { + "content": "Write a memo summarizing this earnings report.", + "role": "user", + } + ], model="palmyra-x-004", stream=True, ) @@ -280,10 +327,9 @@ async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncW chat_stream = await async_client.chat.chat( messages=[ { - "role": "user", "content": "Write a memo summarizing this earnings report.", + "role": "user", "name": "name", - "tool_id": "tool_id", } ], model="palmyra-x-004", @@ -325,7 +371,12 @@ async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncW @parametrize async def test_raw_response_chat_overload_2(self, async_client: AsyncWriter) -> None: response = await async_client.chat.with_raw_response.chat( - messages=[{"role": "user"}], + messages=[ + { + "content": "Write a memo summarizing this earnings report.", + "role": "user", + } + ], model="palmyra-x-004", stream=True, ) @@ -337,7 +388,12 @@ async def test_raw_response_chat_overload_2(self, async_client: AsyncWriter) -> @parametrize async def test_streaming_response_chat_overload_2(self, async_client: AsyncWriter) -> None: async with async_client.chat.with_streaming_response.chat( - messages=[{"role": "user"}], + messages=[ + { + "content": "Write a memo summarizing this earnings report.", + "role": "user", + } + ], model="palmyra-x-004", stream=True, ) as response: diff --git a/tests/test_client.py b/tests/test_client.py index 09eb207e..127f8d58 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -722,7 +722,18 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> No with pytest.raises(APITimeoutError): self.client.post( "/v1/chat", - body=cast(object, dict(messages=[{"role": "user"}], model="palmyra-x-002-32k")), + body=cast( + object, + dict( + messages=[ + { + "content": "Write a memo summarizing this earnings report.", + "role": "user", + } + ], + model="palmyra-x-004", + ), + ), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -737,7 +748,18 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> Non with pytest.raises(APIStatusError): self.client.post( "/v1/chat", - body=cast(object, dict(messages=[{"role": "user"}], model="palmyra-x-002-32k")), + body=cast( + object, + dict( + messages=[ + { + "content": "Write a memo summarizing this earnings report.", + "role": "user", + } + ], + model="palmyra-x-004", + ), + ), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -761,7 +783,15 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) - response = client.chat.with_raw_response.chat(messages=[{"role": "user"}], model="palmyra-x-004") + response = client.chat.with_raw_response.chat( + messages=[ + { + "content": "Write a memo summarizing this earnings report.", + "role": "user", + } + ], + model="palmyra-x-004", + ) assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -786,7 +816,14 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) response = client.chat.with_raw_response.chat( - messages=[{"role": "user"}], model="palmyra-x-004", extra_headers={"x-stainless-retry-count": Omit()} + messages=[ + { + "content": "Write a memo summarizing this earnings report.", + "role": "user", + } + ], + model="palmyra-x-004", + extra_headers={"x-stainless-retry-count": Omit()}, ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -811,7 +848,14 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) response = client.chat.with_raw_response.chat( - messages=[{"role": "user"}], model="palmyra-x-004", extra_headers={"x-stainless-retry-count": "42"} + messages=[ + { + "content": "Write a memo summarizing this earnings report.", + "role": "user", + } + ], + model="palmyra-x-004", + extra_headers={"x-stainless-retry-count": "42"}, ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" @@ -1505,7 +1549,18 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) with pytest.raises(APITimeoutError): await self.client.post( "/v1/chat", - body=cast(object, dict(messages=[{"role": "user"}], model="palmyra-x-002-32k")), + body=cast( + object, + dict( + messages=[ + { + "content": "Write a memo summarizing this earnings report.", + "role": "user", + } + ], + model="palmyra-x-004", + ), + ), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -1520,7 +1575,18 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) with pytest.raises(APIStatusError): await self.client.post( "/v1/chat", - body=cast(object, dict(messages=[{"role": "user"}], model="palmyra-x-002-32k")), + body=cast( + object, + dict( + messages=[ + { + "content": "Write a memo summarizing this earnings report.", + "role": "user", + } + ], + model="palmyra-x-004", + ), + ), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -1547,7 +1613,15 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) - response = await client.chat.with_raw_response.chat(messages=[{"role": "user"}], model="palmyra-x-004") + response = await client.chat.with_raw_response.chat( + messages=[ + { + "content": "Write a memo summarizing this earnings report.", + "role": "user", + } + ], + model="palmyra-x-004", + ) assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -1573,7 +1647,14 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) response = await client.chat.with_raw_response.chat( - messages=[{"role": "user"}], model="palmyra-x-004", extra_headers={"x-stainless-retry-count": Omit()} + messages=[ + { + "content": "Write a memo summarizing this earnings report.", + "role": "user", + } + ], + model="palmyra-x-004", + extra_headers={"x-stainless-retry-count": Omit()}, ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -1599,7 +1680,14 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) response = await client.chat.with_raw_response.chat( - messages=[{"role": "user"}], model="palmyra-x-004", extra_headers={"x-stainless-retry-count": "42"} + messages=[ + { + "content": "Write a memo summarizing this earnings report.", + "role": "user", + } + ], + model="palmyra-x-004", + extra_headers={"x-stainless-retry-count": "42"}, ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" From 40a11fee861d650090050d54deab629accc0b3c3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 9 Oct 2024 12:47:47 +0000 Subject: [PATCH 092/399] chore(internal): version bump (#90) --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 1b77f506..fea34540 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.7.0" + ".": "1.0.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index f9d07992..469f4b98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "0.7.0" +version = "1.0.0" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index e4b348eb..a83feb6c 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "0.7.0" # x-release-please-version +__version__ = "1.0.0" # x-release-please-version From 6a99c5211df50fcb81b3ca71efd710b3e3539175 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 9 Oct 2024 20:45:58 +0000 Subject: [PATCH 093/399] feat(api): api update (#91) --- .stats.yml | 2 +- README.md | 45 ++--------- src/writerai/types/chat_chat_params.py | 6 +- tests/api_resources/test_chat.py | 96 +++++----------------- tests/test_client.py | 108 +++---------------------- 5 files changed, 42 insertions(+), 215 deletions(-) diff --git a/.stats.yml b/.stats.yml index 13f6f3d6..be523581 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-2ad4cffb18a01119bca51bc468740407407dcf2aea29a0323e3c84662817b000.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-330636638e8423bfe0b00d5b5a5f605f9a64c59f27a3a45dd4c7ff88b9411c36.yml diff --git a/README.md b/README.md index e16571cf..bf865c51 100644 --- a/README.md +++ b/README.md @@ -33,12 +33,7 @@ client = Writer( ) chat = client.chat.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-004", ) print(chat.id) @@ -66,12 +61,7 @@ client = AsyncWriter( async def main() -> None: chat = await client.chat.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-004", ) print(chat.id) @@ -205,12 +195,7 @@ client = Writer() try: client.chat.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-004", ) except writerai.APIConnectionError as e: @@ -256,12 +241,7 @@ client = Writer( # Or, configure per-request: client.with_options(max_retries=5).chat.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-004", ) ``` @@ -287,12 +267,7 @@ client = Writer( # Override per-request: client.with_options(timeout=5.0).chat.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-004", ) ``` @@ -335,8 +310,7 @@ from writerai import Writer client = Writer() response = client.chat.with_raw_response.chat( messages=[{ - "content": "Write a memo summarizing this earnings report.", - "role": "user", + "role": "user" }], model="palmyra-x-004", ) @@ -358,12 +332,7 @@ To stream the response body, use `.with_streaming_response` instead, which requi ```python with client.chat.with_streaming_response.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-004", ) as response: print(response.headers.get("X-My-Header")) diff --git a/src/writerai/types/chat_chat_params.py b/src/writerai/types/chat_chat_params.py index 40ac0a13..3e3de6be 100644 --- a/src/writerai/types/chat_chat_params.py +++ b/src/writerai/types/chat_chat_params.py @@ -94,12 +94,14 @@ class ChatChatParamsBase(TypedDict, total=False): class Message(TypedDict, total=False): - content: Required[str] + role: Required[Literal["user", "assistant", "system", "tool"]] - role: Required[Literal["user", "assistant", "system"]] + content: str name: str + tool_id: str + class StreamOptions(TypedDict, total=False): include_usage: Required[bool] diff --git a/tests/api_resources/test_chat.py b/tests/api_resources/test_chat.py index 2bbe527d..c5d36794 100644 --- a/tests/api_resources/test_chat.py +++ b/tests/api_resources/test_chat.py @@ -20,12 +20,7 @@ class TestChat: @parametrize def test_method_chat_overload_1(self, client: Writer) -> None: chat = client.chat.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-004", ) assert_matches_type(Chat, chat, path=["response"]) @@ -35,9 +30,10 @@ def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: chat = client.chat.chat( messages=[ { - "content": "Write a memo summarizing this earnings report.", "role": "user", + "content": "Write a memo summarizing this earnings report.", "name": "name", + "tool_id": "tool_id", } ], model="palmyra-x-004", @@ -79,12 +75,7 @@ def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: @parametrize def test_raw_response_chat_overload_1(self, client: Writer) -> None: response = client.chat.with_raw_response.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-004", ) @@ -96,12 +87,7 @@ def test_raw_response_chat_overload_1(self, client: Writer) -> None: @parametrize def test_streaming_response_chat_overload_1(self, client: Writer) -> None: with client.chat.with_streaming_response.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-004", ) as response: assert not response.is_closed @@ -115,12 +101,7 @@ def test_streaming_response_chat_overload_1(self, client: Writer) -> None: @parametrize def test_method_chat_overload_2(self, client: Writer) -> None: chat_stream = client.chat.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-004", stream=True, ) @@ -131,9 +112,10 @@ def test_method_chat_with_all_params_overload_2(self, client: Writer) -> None: chat_stream = client.chat.chat( messages=[ { - "content": "Write a memo summarizing this earnings report.", "role": "user", + "content": "Write a memo summarizing this earnings report.", "name": "name", + "tool_id": "tool_id", } ], model="palmyra-x-004", @@ -175,12 +157,7 @@ def test_method_chat_with_all_params_overload_2(self, client: Writer) -> None: @parametrize def test_raw_response_chat_overload_2(self, client: Writer) -> None: response = client.chat.with_raw_response.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-004", stream=True, ) @@ -192,12 +169,7 @@ def test_raw_response_chat_overload_2(self, client: Writer) -> None: @parametrize def test_streaming_response_chat_overload_2(self, client: Writer) -> None: with client.chat.with_streaming_response.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-004", stream=True, ) as response: @@ -216,12 +188,7 @@ class TestAsyncChat: @parametrize async def test_method_chat_overload_1(self, async_client: AsyncWriter) -> None: chat = await async_client.chat.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-004", ) assert_matches_type(Chat, chat, path=["response"]) @@ -231,9 +198,10 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW chat = await async_client.chat.chat( messages=[ { - "content": "Write a memo summarizing this earnings report.", "role": "user", + "content": "Write a memo summarizing this earnings report.", "name": "name", + "tool_id": "tool_id", } ], model="palmyra-x-004", @@ -275,12 +243,7 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW @parametrize async def test_raw_response_chat_overload_1(self, async_client: AsyncWriter) -> None: response = await async_client.chat.with_raw_response.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-004", ) @@ -292,12 +255,7 @@ async def test_raw_response_chat_overload_1(self, async_client: AsyncWriter) -> @parametrize async def test_streaming_response_chat_overload_1(self, async_client: AsyncWriter) -> None: async with async_client.chat.with_streaming_response.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-004", ) as response: assert not response.is_closed @@ -311,12 +269,7 @@ async def test_streaming_response_chat_overload_1(self, async_client: AsyncWrite @parametrize async def test_method_chat_overload_2(self, async_client: AsyncWriter) -> None: chat_stream = await async_client.chat.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-004", stream=True, ) @@ -327,9 +280,10 @@ async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncW chat_stream = await async_client.chat.chat( messages=[ { - "content": "Write a memo summarizing this earnings report.", "role": "user", + "content": "Write a memo summarizing this earnings report.", "name": "name", + "tool_id": "tool_id", } ], model="palmyra-x-004", @@ -371,12 +325,7 @@ async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncW @parametrize async def test_raw_response_chat_overload_2(self, async_client: AsyncWriter) -> None: response = await async_client.chat.with_raw_response.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-004", stream=True, ) @@ -388,12 +337,7 @@ async def test_raw_response_chat_overload_2(self, async_client: AsyncWriter) -> @parametrize async def test_streaming_response_chat_overload_2(self, async_client: AsyncWriter) -> None: async with async_client.chat.with_streaming_response.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], + messages=[{"role": "user"}], model="palmyra-x-004", stream=True, ) as response: diff --git a/tests/test_client.py b/tests/test_client.py index 127f8d58..a73fc5ca 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -722,18 +722,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> No with pytest.raises(APITimeoutError): self.client.post( "/v1/chat", - body=cast( - object, - dict( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], - model="palmyra-x-004", - ), - ), + body=cast(object, dict(messages=[{"role": "user"}], model="palmyra-x-004")), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -748,18 +737,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> Non with pytest.raises(APIStatusError): self.client.post( "/v1/chat", - body=cast( - object, - dict( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], - model="palmyra-x-004", - ), - ), + body=cast(object, dict(messages=[{"role": "user"}], model="palmyra-x-004")), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -783,15 +761,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) - response = client.chat.with_raw_response.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], - model="palmyra-x-004", - ) + response = client.chat.with_raw_response.chat(messages=[{"role": "user"}], model="palmyra-x-004") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -816,14 +786,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) response = client.chat.with_raw_response.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], - model="palmyra-x-004", - extra_headers={"x-stainless-retry-count": Omit()}, + messages=[{"role": "user"}], model="palmyra-x-004", extra_headers={"x-stainless-retry-count": Omit()} ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -848,14 +811,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) response = client.chat.with_raw_response.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], - model="palmyra-x-004", - extra_headers={"x-stainless-retry-count": "42"}, + messages=[{"role": "user"}], model="palmyra-x-004", extra_headers={"x-stainless-retry-count": "42"} ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" @@ -1549,18 +1505,7 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) with pytest.raises(APITimeoutError): await self.client.post( "/v1/chat", - body=cast( - object, - dict( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], - model="palmyra-x-004", - ), - ), + body=cast(object, dict(messages=[{"role": "user"}], model="palmyra-x-004")), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -1575,18 +1520,7 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) with pytest.raises(APIStatusError): await self.client.post( "/v1/chat", - body=cast( - object, - dict( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], - model="palmyra-x-004", - ), - ), + body=cast(object, dict(messages=[{"role": "user"}], model="palmyra-x-004")), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -1613,15 +1547,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) - response = await client.chat.with_raw_response.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], - model="palmyra-x-004", - ) + response = await client.chat.with_raw_response.chat(messages=[{"role": "user"}], model="palmyra-x-004") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -1647,14 +1573,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) response = await client.chat.with_raw_response.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], - model="palmyra-x-004", - extra_headers={"x-stainless-retry-count": Omit()}, + messages=[{"role": "user"}], model="palmyra-x-004", extra_headers={"x-stainless-retry-count": Omit()} ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -1680,14 +1599,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) response = await client.chat.with_raw_response.chat( - messages=[ - { - "content": "Write a memo summarizing this earnings report.", - "role": "user", - } - ], - model="palmyra-x-004", - extra_headers={"x-stainless-retry-count": "42"}, + messages=[{"role": "user"}], model="palmyra-x-004", extra_headers={"x-stainless-retry-count": "42"} ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" From 7efc4c7a5171d63a86c5cc54824f96467bfecf8b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 9 Oct 2024 21:20:03 +0000 Subject: [PATCH 094/399] feat(api): api update (#93) --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index fea34540..2601677b 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.0.0" + ".": "1.1.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 469f4b98..3675e998 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "1.0.0" +version = "1.1.0" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index a83feb6c..308cd242 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "1.0.0" # x-release-please-version +__version__ = "1.1.0" # x-release-please-version From d03a6098e84da1cd78017026c9f06fecddb51e3c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 10 Oct 2024 15:24:40 +0000 Subject: [PATCH 095/399] feat(api): api update (#94) --- .stats.yml | 2 +- src/writerai/types/chat_chat_params.py | 2 +- tests/api_resources/test_chat.py | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.stats.yml b/.stats.yml index be523581..be7556de 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-330636638e8423bfe0b00d5b5a5f605f9a64c59f27a3a45dd4c7ff88b9411c36.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-4206be8170d98e1f909e4c0e34167f63952645f292b0e4fa7459ca7fcfc1b48a.yml diff --git a/src/writerai/types/chat_chat_params.py b/src/writerai/types/chat_chat_params.py index 3e3de6be..7ae68090 100644 --- a/src/writerai/types/chat_chat_params.py +++ b/src/writerai/types/chat_chat_params.py @@ -100,7 +100,7 @@ class Message(TypedDict, total=False): name: str - tool_id: str + tool_call_id: str class StreamOptions(TypedDict, total=False): diff --git a/tests/api_resources/test_chat.py b/tests/api_resources/test_chat.py index c5d36794..e7db61c7 100644 --- a/tests/api_resources/test_chat.py +++ b/tests/api_resources/test_chat.py @@ -33,7 +33,7 @@ def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: "role": "user", "content": "Write a memo summarizing this earnings report.", "name": "name", - "tool_id": "tool_id", + "tool_call_id": "tool_call_id", } ], model="palmyra-x-004", @@ -115,7 +115,7 @@ def test_method_chat_with_all_params_overload_2(self, client: Writer) -> None: "role": "user", "content": "Write a memo summarizing this earnings report.", "name": "name", - "tool_id": "tool_id", + "tool_call_id": "tool_call_id", } ], model="palmyra-x-004", @@ -201,7 +201,7 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW "role": "user", "content": "Write a memo summarizing this earnings report.", "name": "name", - "tool_id": "tool_id", + "tool_call_id": "tool_call_id", } ], model="palmyra-x-004", @@ -283,7 +283,7 @@ async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncW "role": "user", "content": "Write a memo summarizing this earnings report.", "name": "name", - "tool_id": "tool_id", + "tool_call_id": "tool_call_id", } ], model="palmyra-x-004", From b4cd59f26eb70bfaac28783fe19a8d202144dacf Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 10 Oct 2024 18:28:52 +0000 Subject: [PATCH 096/399] feat(api): api update (#96) --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 2601677b..d0ab6645 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.1.0" + ".": "1.2.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 3675e998..e9915f00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "1.1.0" +version = "1.2.0" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index 308cd242..2c354a52 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "1.1.0" # x-release-please-version +__version__ = "1.2.0" # x-release-please-version From 325d4eb91d419e096b316dc2d6d491c421d3ae7d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 24 Oct 2024 18:28:27 +0000 Subject: [PATCH 097/399] feat(api): api update (#97) --- .stats.yml | 2 +- pyproject.toml | 8 +- requirements-dev.lock | 2 +- src/writerai/_base_client.py | 2 +- src/writerai/resources/chat.py | 54 ++++----- src/writerai/types/chat.py | 104 +++++++++--------- src/writerai/types/chat_chat_params.py | 29 +++-- src/writerai/types/chat_completion_chunk.py | 6 +- src/writerai/types/file_delete_response.py | 1 - src/writerai/types/graph_delete_response.py | 1 - .../graph_remove_file_from_graph_response.py | 1 - src/writerai/types/streaming_data.py | 1 - tests/api_resources/test_chat.py | 44 +++++--- tests/test_client.py | 21 +++- tests/test_models.py | 2 +- 15 files changed, 150 insertions(+), 128 deletions(-) diff --git a/.stats.yml b/.stats.yml index be7556de..c0180f5b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-4206be8170d98e1f909e4c0e34167f63952645f292b0e4fa7459ca7fcfc1b48a.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-b508bc085393c028e9caac93c923305a558a3f1b059bf990a712a69d4ef58dfa.yml diff --git a/pyproject.toml b/pyproject.toml index e9915f00..0cb09bd9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,11 +63,11 @@ format = { chain = [ "format:ruff", "format:docs", "fix:ruff", + # run formatting again to fix any inconsistencies when imports are stripped + "format:ruff", ]} -"format:black" = "black ." "format:docs" = "python scripts/utils/ruffen-docs.py README.md api.md" "format:ruff" = "ruff format" -"format:isort" = "isort ." "lint" = { chain = [ "check:ruff", @@ -125,10 +125,6 @@ path = "README.md" pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)' replacement = '[\1](https://github.com/writer/writer-python/tree/main/\g<2>)' -[tool.black] -line-length = 120 -target-version = ["py37"] - [tool.pytest.ini_options] testpaths = ["tests"] addopts = "--tb=short" diff --git a/requirements-dev.lock b/requirements-dev.lock index d3ee1e6d..fb7515d3 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -80,7 +80,7 @@ pytz==2023.3.post1 # via dirty-equals respx==0.20.2 rich==13.7.1 -ruff==0.6.5 +ruff==0.6.9 setuptools==68.2.2 # via nodeenv six==1.16.0 diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index 9c96f633..520a439c 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -1575,7 +1575,7 @@ async def _request( except Exception as err: log.debug("Encountered Exception", exc_info=True) - if retries_taken > 0: + if remaining_retries > 0: return await self._retry_request( input_options, cast_to, diff --git a/src/writerai/resources/chat.py b/src/writerai/resources/chat.py index a276e985..005aabad 100644 --- a/src/writerai/resources/chat.py +++ b/src/writerai/resources/chat.py @@ -112,12 +112,11 @@ def chat( tool_choice: Configure how the model will call functions: `auto` will allow the model to automatically choose the best tool, `none` disables tool calling. You can also - pass a specific previously defined function as a string. + pass a specific previously defined function. - tools: [Beta] An array of tools described to the model using JSON schema that the model - can use to generate responses. Please note that tool calling is in beta and - subject to change. Passing graph IDs will automatically use the Knowledge Graph - tool. + tools: An array of tools described to the model using JSON schema that the model can + use to generate responses. Passing graph IDs will automatically use the + Knowledge Graph tool. top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with @@ -196,12 +195,11 @@ def chat( tool_choice: Configure how the model will call functions: `auto` will allow the model to automatically choose the best tool, `none` disables tool calling. You can also - pass a specific previously defined function as a string. + pass a specific previously defined function. - tools: [Beta] An array of tools described to the model using JSON schema that the model - can use to generate responses. Please note that tool calling is in beta and - subject to change. Passing graph IDs will automatically use the Knowledge Graph - tool. + tools: An array of tools described to the model using JSON schema that the model can + use to generate responses. Passing graph IDs will automatically use the + Knowledge Graph tool. top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with @@ -280,12 +278,11 @@ def chat( tool_choice: Configure how the model will call functions: `auto` will allow the model to automatically choose the best tool, `none` disables tool calling. You can also - pass a specific previously defined function as a string. + pass a specific previously defined function. - tools: [Beta] An array of tools described to the model using JSON schema that the model - can use to generate responses. Please note that tool calling is in beta and - subject to change. Passing graph IDs will automatically use the Knowledge Graph - tool. + tools: An array of tools described to the model using JSON schema that the model can + use to generate responses. Passing graph IDs will automatically use the + Knowledge Graph tool. top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with @@ -435,12 +432,11 @@ async def chat( tool_choice: Configure how the model will call functions: `auto` will allow the model to automatically choose the best tool, `none` disables tool calling. You can also - pass a specific previously defined function as a string. + pass a specific previously defined function. - tools: [Beta] An array of tools described to the model using JSON schema that the model - can use to generate responses. Please note that tool calling is in beta and - subject to change. Passing graph IDs will automatically use the Knowledge Graph - tool. + tools: An array of tools described to the model using JSON schema that the model can + use to generate responses. Passing graph IDs will automatically use the + Knowledge Graph tool. top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with @@ -519,12 +515,11 @@ async def chat( tool_choice: Configure how the model will call functions: `auto` will allow the model to automatically choose the best tool, `none` disables tool calling. You can also - pass a specific previously defined function as a string. + pass a specific previously defined function. - tools: [Beta] An array of tools described to the model using JSON schema that the model - can use to generate responses. Please note that tool calling is in beta and - subject to change. Passing graph IDs will automatically use the Knowledge Graph - tool. + tools: An array of tools described to the model using JSON schema that the model can + use to generate responses. Passing graph IDs will automatically use the + Knowledge Graph tool. top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with @@ -603,12 +598,11 @@ async def chat( tool_choice: Configure how the model will call functions: `auto` will allow the model to automatically choose the best tool, `none` disables tool calling. You can also - pass a specific previously defined function as a string. + pass a specific previously defined function. - tools: [Beta] An array of tools described to the model using JSON schema that the model - can use to generate responses. Please note that tool calling is in beta and - subject to change. Passing graph IDs will automatically use the Knowledge Graph - tool. + tools: An array of tools described to the model using JSON schema that the model can + use to generate responses. Passing graph IDs will automatically use the + Knowledge Graph tool. top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with diff --git a/src/writerai/types/chat.py b/src/writerai/types/chat.py index dae86096..48f7fd73 100644 --- a/src/writerai/types/chat.py +++ b/src/writerai/types/chat.py @@ -8,11 +8,6 @@ __all__ = [ "Chat", "Choice", - "ChoiceLogprobs", - "ChoiceLogprobsContent", - "ChoiceLogprobsContentTopLogprob", - "ChoiceLogprobsRefusal", - "ChoiceLogprobsRefusalTopLogprob", "ChoiceMessage", "ChoiceMessageGraphData", "ChoiceMessageGraphDataSource", @@ -20,54 +15,17 @@ "ChoiceMessageGraphDataSubquerySource", "ChoiceMessageToolCall", "ChoiceMessageToolCallFunction", + "ChoiceLogprobs", + "ChoiceLogprobsContent", + "ChoiceLogprobsContentTopLogprob", + "ChoiceLogprobsRefusal", + "ChoiceLogprobsRefusalTopLogprob", "Usage", "UsageCompletionTokensDetails", "UsagePromptTokenDetails", ] -class ChoiceLogprobsContentTopLogprob(BaseModel): - token: str - - logprob: float - - bytes: Optional[List[int]] = None - - -class ChoiceLogprobsContent(BaseModel): - token: str - - logprob: float - - top_logprobs: List[ChoiceLogprobsContentTopLogprob] - - bytes: Optional[List[int]] = None - - -class ChoiceLogprobsRefusalTopLogprob(BaseModel): - token: str - - logprob: float - - bytes: Optional[List[int]] = None - - -class ChoiceLogprobsRefusal(BaseModel): - token: str - - logprob: float - - top_logprobs: List[ChoiceLogprobsRefusalTopLogprob] - - bytes: Optional[List[int]] = None - - -class ChoiceLogprobs(BaseModel): - content: Optional[List[ChoiceLogprobsContent]] = None - - refusal: Optional[List[ChoiceLogprobsRefusal]] = None - - class ChoiceMessageGraphDataSource(BaseModel): file_id: str """The unique identifier of the file.""" @@ -126,7 +84,7 @@ class ChoiceMessage(BaseModel): to the input query or command. """ - refusal: str + refusal: Optional[str] = None role: Literal["assistant"] """Specifies the role associated with the content.""" @@ -136,6 +94,48 @@ class ChoiceMessage(BaseModel): tool_calls: Optional[List[ChoiceMessageToolCall]] = None +class ChoiceLogprobsContentTopLogprob(BaseModel): + token: str + + logprob: float + + bytes: Optional[List[int]] = None + + +class ChoiceLogprobsContent(BaseModel): + token: str + + logprob: float + + top_logprobs: List[ChoiceLogprobsContentTopLogprob] + + bytes: Optional[List[int]] = None + + +class ChoiceLogprobsRefusalTopLogprob(BaseModel): + token: str + + logprob: float + + bytes: Optional[List[int]] = None + + +class ChoiceLogprobsRefusal(BaseModel): + token: str + + logprob: float + + top_logprobs: List[ChoiceLogprobsRefusalTopLogprob] + + bytes: Optional[List[int]] = None + + +class ChoiceLogprobs(BaseModel): + content: Optional[List[ChoiceLogprobsContent]] = None + + refusal: Optional[List[ChoiceLogprobsRefusal]] = None + + class Choice(BaseModel): finish_reason: Literal["stop", "length", "content_filter", "tool_calls"] """Describes the condition under which the model ceased generating content. @@ -148,15 +148,15 @@ class Choice(BaseModel): index: int """The index of the choice in the list of completions generated by the model.""" - logprobs: ChoiceLogprobs - """Log probability information for the choice.""" - message: ChoiceMessage """The chat completion message from the model. Note: this field is deprecated for streaming. Use `delta` instead. """ + logprobs: Optional[ChoiceLogprobs] = None + """Log probability information for the choice.""" + class UsageCompletionTokensDetails(BaseModel): reasoning_tokens: int @@ -202,7 +202,7 @@ class Chat(BaseModel): model: str """Identifies the specific model used to generate the response.""" - object: str + object: Literal["chat.completion"] """ The type of object returned, which is always `chat.completion` for chat responses. diff --git a/src/writerai/types/chat_chat_params.py b/src/writerai/types/chat_chat_params.py index 7ae68090..2cc9093a 100644 --- a/src/writerai/types/chat_chat_params.py +++ b/src/writerai/types/chat_chat_params.py @@ -10,8 +10,8 @@ "Message", "StreamOptions", "ToolChoice", - "ToolChoiceJsonObjectToolChoice", "ToolChoiceStringToolChoice", + "ToolChoiceJsonObjectToolChoice", "Tool", "ToolFunctionTool", "ToolFunctionToolFunction", @@ -73,15 +73,14 @@ class ChatChatParamsBase(TypedDict, total=False): """ Configure how the model will call functions: `auto` will allow the model to automatically choose the best tool, `none` disables tool calling. You can also - pass a specific previously defined function as a string. + pass a specific previously defined function. """ tools: Iterable[Tool] """ - [Beta] An array of tools described to the model using JSON schema that the model - can use to generate responses. Please note that tool calling is in beta and - subject to change. Passing graph IDs will automatically use the Knowledge Graph - tool. + An array of tools described to the model using JSON schema that the model can + use to generate responses. Passing graph IDs will automatically use the + Knowledge Graph tool. """ top_p: float @@ -108,21 +107,23 @@ class StreamOptions(TypedDict, total=False): """Indicate whether to include usage information.""" -class ToolChoiceJsonObjectToolChoice(TypedDict, total=False): - value: Required[object] - - class ToolChoiceStringToolChoice(TypedDict, total=False): value: Required[Literal["none", "auto", "required"]] -ToolChoice: TypeAlias = Union[ToolChoiceJsonObjectToolChoice, ToolChoiceStringToolChoice] +class ToolChoiceJsonObjectToolChoice(TypedDict, total=False): + value: Required[object] + + +ToolChoice: TypeAlias = Union[ToolChoiceStringToolChoice, ToolChoiceJsonObjectToolChoice] class ToolFunctionToolFunction(TypedDict, total=False): name: Required[str] + """Name of the function""" description: str + """Description of the function""" parameters: object @@ -130,6 +131,9 @@ class ToolFunctionToolFunction(TypedDict, total=False): class ToolFunctionTool(TypedDict, total=False): function: Required[ToolFunctionToolFunction] + type: Required[Literal["function"]] + """The type of tool.""" + class ToolGraphToolFunction(TypedDict, total=False): graph_ids: Required[List[str]] @@ -145,6 +149,9 @@ class ToolGraphToolFunction(TypedDict, total=False): class ToolGraphTool(TypedDict, total=False): function: Required[ToolGraphToolFunction] + type: Required[Literal["graph"]] + """The type of tool.""" + Tool: TypeAlias = Union[ToolFunctionTool, ToolGraphTool] diff --git a/src/writerai/types/chat_completion_chunk.py b/src/writerai/types/chat_completion_chunk.py index 969b67a1..0a6fe71a 100644 --- a/src/writerai/types/chat_completion_chunk.py +++ b/src/writerai/types/chat_completion_chunk.py @@ -205,7 +205,7 @@ class ChoiceMessage(BaseModel): to the input query or command. """ - refusal: str + refusal: Optional[str] = None role: Literal["assistant"] """Specifies the role associated with the content.""" @@ -219,7 +219,7 @@ class Choice(BaseModel): delta: ChoiceDelta """A chat completion delta generated by streamed model responses.""" - finish_reason: Literal["stop", "length", "content_filter", "tool_calls"] + finish_reason: Optional[Literal["stop", "length", "content_filter", "tool_calls"]] = None """Describes the condition under which the model ceased generating content. Common reasons include 'length' (reached the maximum output size), 'stop' @@ -284,7 +284,7 @@ class ChatCompletionChunk(BaseModel): model: str """Identifies the specific model used to generate the response.""" - object: str + object: Literal["chat.completion.chunk"] """ The type of object returned, which is always `chat.completion.chunk` for streaming chat responses. diff --git a/src/writerai/types/file_delete_response.py b/src/writerai/types/file_delete_response.py index 7c359f8e..1bea530d 100644 --- a/src/writerai/types/file_delete_response.py +++ b/src/writerai/types/file_delete_response.py @@ -1,7 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - from .._models import BaseModel __all__ = ["FileDeleteResponse"] diff --git a/src/writerai/types/graph_delete_response.py b/src/writerai/types/graph_delete_response.py index dd849fa3..a91b734c 100644 --- a/src/writerai/types/graph_delete_response.py +++ b/src/writerai/types/graph_delete_response.py @@ -1,7 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - from .._models import BaseModel __all__ = ["GraphDeleteResponse"] diff --git a/src/writerai/types/graph_remove_file_from_graph_response.py b/src/writerai/types/graph_remove_file_from_graph_response.py index 7fa56f72..83711a69 100644 --- a/src/writerai/types/graph_remove_file_from_graph_response.py +++ b/src/writerai/types/graph_remove_file_from_graph_response.py @@ -1,7 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - from .._models import BaseModel __all__ = ["GraphRemoveFileFromGraphResponse"] diff --git a/src/writerai/types/streaming_data.py b/src/writerai/types/streaming_data.py index 966912bd..8c88b456 100644 --- a/src/writerai/types/streaming_data.py +++ b/src/writerai/types/streaming_data.py @@ -1,7 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - from .._models import BaseModel __all__ = ["StreamingData"] diff --git a/tests/api_resources/test_chat.py b/tests/api_resources/test_chat.py index e7db61c7..ad6c3d30 100644 --- a/tests/api_resources/test_chat.py +++ b/tests/api_resources/test_chat.py @@ -44,28 +44,31 @@ def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: stream=False, stream_options={"include_usage": True}, temperature=0, - tool_choice={"value": {}}, + tool_choice={"value": "none"}, tools=[ { "function": { "name": "name", "description": "description", "parameters": {}, - } + }, + "type": "function", }, { "function": { "name": "name", "description": "description", "parameters": {}, - } + }, + "type": "function", }, { "function": { "name": "name", "description": "description", "parameters": {}, - } + }, + "type": "function", }, ], top_p=0, @@ -126,28 +129,31 @@ def test_method_chat_with_all_params_overload_2(self, client: Writer) -> None: stop=["string", "string", "string"], stream_options={"include_usage": True}, temperature=0, - tool_choice={"value": {}}, + tool_choice={"value": "none"}, tools=[ { "function": { "name": "name", "description": "description", "parameters": {}, - } + }, + "type": "function", }, { "function": { "name": "name", "description": "description", "parameters": {}, - } + }, + "type": "function", }, { "function": { "name": "name", "description": "description", "parameters": {}, - } + }, + "type": "function", }, ], top_p=0, @@ -212,28 +218,31 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW stream=False, stream_options={"include_usage": True}, temperature=0, - tool_choice={"value": {}}, + tool_choice={"value": "none"}, tools=[ { "function": { "name": "name", "description": "description", "parameters": {}, - } + }, + "type": "function", }, { "function": { "name": "name", "description": "description", "parameters": {}, - } + }, + "type": "function", }, { "function": { "name": "name", "description": "description", "parameters": {}, - } + }, + "type": "function", }, ], top_p=0, @@ -294,28 +303,31 @@ async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncW stop=["string", "string", "string"], stream_options={"include_usage": True}, temperature=0, - tool_choice={"value": {}}, + tool_choice={"value": "none"}, tools=[ { "function": { "name": "name", "description": "description", "parameters": {}, - } + }, + "type": "function", }, { "function": { "name": "name", "description": "description", "parameters": {}, - } + }, + "type": "function", }, { "function": { "name": "name", "description": "description", "parameters": {}, - } + }, + "type": "function", }, ], top_p=0, diff --git a/tests/test_client.py b/tests/test_client.py index a73fc5ca..92e90eec 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -10,6 +10,7 @@ import tracemalloc from typing import Any, Union, cast from unittest import mock +from typing_extensions import Literal import httpx import pytest @@ -747,7 +748,14 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> Non @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @mock.patch("writerai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - def test_retries_taken(self, client: Writer, failures_before_success: int, respx_mock: MockRouter) -> None: + @pytest.mark.parametrize("failure_mode", ["status", "exception"]) + def test_retries_taken( + self, + client: Writer, + failures_before_success: int, + failure_mode: Literal["status", "exception"], + respx_mock: MockRouter, + ) -> None: client = client.with_options(max_retries=4) nb_retries = 0 @@ -756,6 +764,8 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: nonlocal nb_retries if nb_retries < failures_before_success: nb_retries += 1 + if failure_mode == "exception": + raise RuntimeError("oops") return httpx.Response(500) return httpx.Response(200) @@ -1531,8 +1541,13 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) @mock.patch("writerai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) @pytest.mark.asyncio + @pytest.mark.parametrize("failure_mode", ["status", "exception"]) async def test_retries_taken( - self, async_client: AsyncWriter, failures_before_success: int, respx_mock: MockRouter + self, + async_client: AsyncWriter, + failures_before_success: int, + failure_mode: Literal["status", "exception"], + respx_mock: MockRouter, ) -> None: client = async_client.with_options(max_retries=4) @@ -1542,6 +1557,8 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: nonlocal nb_retries if nb_retries < failures_before_success: nb_retries += 1 + if failure_mode == "exception": + raise RuntimeError("oops") return httpx.Response(500) return httpx.Response(200) diff --git a/tests/test_models.py b/tests/test_models.py index fa64230d..61c86a0b 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -245,7 +245,7 @@ class Model(BaseModel): assert m.foo is True m = Model.construct(foo="CARD_HOLDER") - assert m.foo is "CARD_HOLDER" + assert m.foo == "CARD_HOLDER" m = Model.construct(foo={"bar": False}) assert isinstance(m.foo, Submodel1) From 881c72f661f6c83ddf84931b78276fa601fd1938 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 30 Oct 2024 18:52:51 +0000 Subject: [PATCH 098/399] chore: rebuild project due to codegen change (#99) --- requirements-dev.lock | 21 +++++++++------------ requirements.lock | 8 ++++---- src/writerai/_compat.py | 2 +- src/writerai/_models.py | 10 +++++----- src/writerai/_types.py | 6 ++++-- tests/conftest.py | 14 ++++++++------ 6 files changed, 31 insertions(+), 30 deletions(-) diff --git a/requirements-dev.lock b/requirements-dev.lock index fb7515d3..eef2556c 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -16,8 +16,6 @@ anyio==4.4.0 # via writer-sdk argcomplete==3.1.2 # via nox -attrs==23.1.0 - # via pytest certifi==2023.7.22 # via httpcore # via httpx @@ -28,8 +26,9 @@ distlib==0.3.7 # via virtualenv distro==1.8.0 # via writer-sdk -exceptiongroup==1.1.3 +exceptiongroup==1.2.2 # via anyio + # via pytest filelock==3.12.4 # via virtualenv h11==0.14.0 @@ -60,20 +59,18 @@ packaging==23.2 # via pytest platformdirs==3.11.0 # via virtualenv -pluggy==1.3.0 - # via pytest -py==1.11.0 +pluggy==1.5.0 # via pytest -pydantic==2.7.1 +pydantic==2.9.2 # via writer-sdk -pydantic-core==2.18.2 +pydantic-core==2.23.4 # via pydantic pygments==2.18.0 # via rich pyright==1.1.380 -pytest==7.1.1 +pytest==8.3.3 # via pytest-asyncio -pytest-asyncio==0.21.1 +pytest-asyncio==0.24.0 python-dateutil==2.8.2 # via time-machine pytz==2023.3.post1 @@ -90,10 +87,10 @@ sniffio==1.3.0 # via httpx # via writer-sdk time-machine==2.9.0 -tomli==2.0.1 +tomli==2.0.2 # via mypy # via pytest -typing-extensions==4.8.0 +typing-extensions==4.12.2 # via anyio # via mypy # via pydantic diff --git a/requirements.lock b/requirements.lock index b2984eab..0091397a 100644 --- a/requirements.lock +++ b/requirements.lock @@ -19,7 +19,7 @@ certifi==2023.7.22 # via httpx distro==1.8.0 # via writer-sdk -exceptiongroup==1.1.3 +exceptiongroup==1.2.2 # via anyio h11==0.14.0 # via httpcore @@ -30,15 +30,15 @@ httpx==0.25.2 idna==3.4 # via anyio # via httpx -pydantic==2.7.1 +pydantic==2.9.2 # via writer-sdk -pydantic-core==2.18.2 +pydantic-core==2.23.4 # via pydantic sniffio==1.3.0 # via anyio # via httpx # via writer-sdk -typing-extensions==4.8.0 +typing-extensions==4.12.2 # via anyio # via pydantic # via pydantic-core diff --git a/src/writerai/_compat.py b/src/writerai/_compat.py index 162a6fbe..d89920d9 100644 --- a/src/writerai/_compat.py +++ b/src/writerai/_compat.py @@ -133,7 +133,7 @@ def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str: def model_dump( model: pydantic.BaseModel, *, - exclude: IncEx = None, + exclude: IncEx | None = None, exclude_unset: bool = False, exclude_defaults: bool = False, warnings: bool = True, diff --git a/src/writerai/_models.py b/src/writerai/_models.py index d386eaa3..42551b76 100644 --- a/src/writerai/_models.py +++ b/src/writerai/_models.py @@ -176,7 +176,7 @@ def __str__(self) -> str: # Based on https://github.com/samuelcolvin/pydantic/issues/1168#issuecomment-817742836. @classmethod @override - def construct( + def construct( # pyright: ignore[reportIncompatibleMethodOverride] cls: Type[ModelT], _fields_set: set[str] | None = None, **values: object, @@ -248,8 +248,8 @@ def model_dump( self, *, mode: Literal["json", "python"] | str = "python", - include: IncEx = None, - exclude: IncEx = None, + include: IncEx | None = None, + exclude: IncEx | None = None, by_alias: bool = False, exclude_unset: bool = False, exclude_defaults: bool = False, @@ -303,8 +303,8 @@ def model_dump_json( self, *, indent: int | None = None, - include: IncEx = None, - exclude: IncEx = None, + include: IncEx | None = None, + exclude: IncEx | None = None, by_alias: bool = False, exclude_unset: bool = False, exclude_defaults: bool = False, diff --git a/src/writerai/_types.py b/src/writerai/_types.py index 35538cb8..20b15b83 100644 --- a/src/writerai/_types.py +++ b/src/writerai/_types.py @@ -16,7 +16,7 @@ Optional, Sequence, ) -from typing_extensions import Literal, Protocol, TypeAlias, TypedDict, override, runtime_checkable +from typing_extensions import Set, Literal, Protocol, TypeAlias, TypedDict, override, runtime_checkable import httpx import pydantic @@ -193,7 +193,9 @@ def get(self, __key: str) -> str | None: ... # Note: copied from Pydantic # https://github.com/pydantic/pydantic/blob/32ea570bf96e84234d2992e1ddf40ab8a565925a/pydantic/main.py#L49 -IncEx: TypeAlias = "set[int] | set[str] | dict[int, Any] | dict[str, Any] | None" +IncEx: TypeAlias = Union[ + Set[int], Set[str], Mapping[int, Union["IncEx", Literal[True]]], Mapping[str, Union["IncEx", Literal[True]]] +] PostParser = Callable[[Any], Any] diff --git a/tests/conftest.py b/tests/conftest.py index 5acea6d5..573ff773 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,11 +1,11 @@ from __future__ import annotations import os -import asyncio import logging from typing import TYPE_CHECKING, Iterator, AsyncIterator import pytest +from pytest_asyncio import is_async_test from writerai import Writer, AsyncWriter @@ -17,11 +17,13 @@ logging.getLogger("writerai").setLevel(logging.DEBUG) -@pytest.fixture(scope="session") -def event_loop() -> Iterator[asyncio.AbstractEventLoop]: - loop = asyncio.new_event_loop() - yield loop - loop.close() +# automatically add `pytest.mark.asyncio()` to all of our async tests +# so we don't have to add that boilerplate everywhere +def pytest_collection_modifyitems(items: list[pytest.Function]) -> None: + pytest_asyncio_tests = (item for item in items if is_async_test(item)) + session_scope_marker = pytest.mark.asyncio(loop_scope="session") + for async_test in pytest_asyncio_tests: + async_test.add_marker(session_scope_marker, append=False) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") From 0680ae5ef617227b8461c8e9b9e33abc7e946d77 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 30 Oct 2024 20:17:41 +0000 Subject: [PATCH 099/399] chore: rebuild project due to codegen change (#100) --- .stats.yml | 2 +- src/writerai/types/chat_chat_params.py | 70 ++++- tests/api_resources/test_chat.py | 340 +++++++++++++++++++++++++ 3 files changed, 407 insertions(+), 5 deletions(-) diff --git a/.stats.yml b/.stats.yml index c0180f5b..daefc3e5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-b508bc085393c028e9caac93c923305a558a3f1b059bf990a712a69d4ef58dfa.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-94a38a74ceffcba2040259f19be51ccf509cb5b6560f094f5a2312a2ef1f78b1.yml diff --git a/src/writerai/types/chat_chat_params.py b/src/writerai/types/chat_chat_params.py index 2cc9093a..0b453e91 100644 --- a/src/writerai/types/chat_chat_params.py +++ b/src/writerai/types/chat_chat_params.py @@ -2,12 +2,18 @@ from __future__ import annotations -from typing import List, Union, Iterable +from typing import List, Union, Iterable, Optional from typing_extensions import Literal, Required, TypeAlias, TypedDict __all__ = [ "ChatChatParamsBase", "Message", + "MessageGraphData", + "MessageGraphDataSource", + "MessageGraphDataSubquery", + "MessageGraphDataSubquerySource", + "MessageToolCall", + "MessageToolCallFunction", "StreamOptions", "ToolChoice", "ToolChoiceStringToolChoice", @@ -92,14 +98,70 @@ class ChatChatParamsBase(TypedDict, total=False): """ +class MessageGraphDataSource(TypedDict, total=False): + file_id: Required[str] + """The unique identifier of the file.""" + + snippet: Required[str] + """A snippet of text from the source file.""" + + +class MessageGraphDataSubquerySource(TypedDict, total=False): + file_id: Required[str] + """The unique identifier of the file.""" + + snippet: Required[str] + """A snippet of text from the source file.""" + + +class MessageGraphDataSubquery(TypedDict, total=False): + answer: Required[str] + """The answer to the subquery.""" + + query: Required[str] + """The subquery that was asked.""" + + sources: Required[Iterable[MessageGraphDataSubquerySource]] + + +class MessageGraphData(TypedDict, total=False): + sources: Iterable[MessageGraphDataSource] + + status: Literal["processing", "finished"] + + subqueries: Iterable[MessageGraphDataSubquery] + + +class MessageToolCallFunction(TypedDict, total=False): + arguments: Required[str] + + name: Required[str] + + +class MessageToolCall(TypedDict, total=False): + id: Required[str] + + function: Required[MessageToolCallFunction] + + type: Required[str] + + index: int + + class Message(TypedDict, total=False): role: Required[Literal["user", "assistant", "system", "tool"]] - content: str + content: Optional[str] + + graph_data: Optional[MessageGraphData] + + name: Optional[str] + + refusal: Optional[str] - name: str + tool_call_id: Optional[str] - tool_call_id: str + tool_calls: Optional[Iterable[MessageToolCall]] class StreamOptions(TypedDict, total=False): diff --git a/tests/api_resources/test_chat.py b/tests/api_resources/test_chat.py index ad6c3d30..39338d19 100644 --- a/tests/api_resources/test_chat.py +++ b/tests/api_resources/test_chat.py @@ -32,8 +32,93 @@ def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: { "role": "user", "content": "Write a memo summarizing this earnings report.", + "graph_data": { + "sources": [ + { + "file_id": "file_id", + "snippet": "snippet", + }, + { + "file_id": "file_id", + "snippet": "snippet", + }, + { + "file_id": "file_id", + "snippet": "snippet", + }, + ], + "status": "processing", + "subqueries": [ + { + "answer": "answer", + "query": "query", + "sources": [ + { + "file_id": "file_id", + "snippet": "snippet", + }, + { + "file_id": "file_id", + "snippet": "snippet", + }, + { + "file_id": "file_id", + "snippet": "snippet", + }, + ], + }, + { + "answer": "answer", + "query": "query", + "sources": [ + { + "file_id": "file_id", + "snippet": "snippet", + }, + { + "file_id": "file_id", + "snippet": "snippet", + }, + { + "file_id": "file_id", + "snippet": "snippet", + }, + ], + }, + { + "answer": "answer", + "query": "query", + "sources": [ + { + "file_id": "file_id", + "snippet": "snippet", + }, + { + "file_id": "file_id", + "snippet": "snippet", + }, + { + "file_id": "file_id", + "snippet": "snippet", + }, + ], + }, + ], + }, "name": "name", + "refusal": "refusal", "tool_call_id": "tool_call_id", + "tool_calls": [ + { + "id": "id", + "function": { + "arguments": "arguments", + "name": "name", + }, + "type": "type", + "index": 0, + } + ], } ], model="palmyra-x-004", @@ -117,8 +202,93 @@ def test_method_chat_with_all_params_overload_2(self, client: Writer) -> None: { "role": "user", "content": "Write a memo summarizing this earnings report.", + "graph_data": { + "sources": [ + { + "file_id": "file_id", + "snippet": "snippet", + }, + { + "file_id": "file_id", + "snippet": "snippet", + }, + { + "file_id": "file_id", + "snippet": "snippet", + }, + ], + "status": "processing", + "subqueries": [ + { + "answer": "answer", + "query": "query", + "sources": [ + { + "file_id": "file_id", + "snippet": "snippet", + }, + { + "file_id": "file_id", + "snippet": "snippet", + }, + { + "file_id": "file_id", + "snippet": "snippet", + }, + ], + }, + { + "answer": "answer", + "query": "query", + "sources": [ + { + "file_id": "file_id", + "snippet": "snippet", + }, + { + "file_id": "file_id", + "snippet": "snippet", + }, + { + "file_id": "file_id", + "snippet": "snippet", + }, + ], + }, + { + "answer": "answer", + "query": "query", + "sources": [ + { + "file_id": "file_id", + "snippet": "snippet", + }, + { + "file_id": "file_id", + "snippet": "snippet", + }, + { + "file_id": "file_id", + "snippet": "snippet", + }, + ], + }, + ], + }, "name": "name", + "refusal": "refusal", "tool_call_id": "tool_call_id", + "tool_calls": [ + { + "id": "id", + "function": { + "arguments": "arguments", + "name": "name", + }, + "type": "type", + "index": 0, + } + ], } ], model="palmyra-x-004", @@ -206,8 +376,93 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW { "role": "user", "content": "Write a memo summarizing this earnings report.", + "graph_data": { + "sources": [ + { + "file_id": "file_id", + "snippet": "snippet", + }, + { + "file_id": "file_id", + "snippet": "snippet", + }, + { + "file_id": "file_id", + "snippet": "snippet", + }, + ], + "status": "processing", + "subqueries": [ + { + "answer": "answer", + "query": "query", + "sources": [ + { + "file_id": "file_id", + "snippet": "snippet", + }, + { + "file_id": "file_id", + "snippet": "snippet", + }, + { + "file_id": "file_id", + "snippet": "snippet", + }, + ], + }, + { + "answer": "answer", + "query": "query", + "sources": [ + { + "file_id": "file_id", + "snippet": "snippet", + }, + { + "file_id": "file_id", + "snippet": "snippet", + }, + { + "file_id": "file_id", + "snippet": "snippet", + }, + ], + }, + { + "answer": "answer", + "query": "query", + "sources": [ + { + "file_id": "file_id", + "snippet": "snippet", + }, + { + "file_id": "file_id", + "snippet": "snippet", + }, + { + "file_id": "file_id", + "snippet": "snippet", + }, + ], + }, + ], + }, "name": "name", + "refusal": "refusal", "tool_call_id": "tool_call_id", + "tool_calls": [ + { + "id": "id", + "function": { + "arguments": "arguments", + "name": "name", + }, + "type": "type", + "index": 0, + } + ], } ], model="palmyra-x-004", @@ -291,8 +546,93 @@ async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncW { "role": "user", "content": "Write a memo summarizing this earnings report.", + "graph_data": { + "sources": [ + { + "file_id": "file_id", + "snippet": "snippet", + }, + { + "file_id": "file_id", + "snippet": "snippet", + }, + { + "file_id": "file_id", + "snippet": "snippet", + }, + ], + "status": "processing", + "subqueries": [ + { + "answer": "answer", + "query": "query", + "sources": [ + { + "file_id": "file_id", + "snippet": "snippet", + }, + { + "file_id": "file_id", + "snippet": "snippet", + }, + { + "file_id": "file_id", + "snippet": "snippet", + }, + ], + }, + { + "answer": "answer", + "query": "query", + "sources": [ + { + "file_id": "file_id", + "snippet": "snippet", + }, + { + "file_id": "file_id", + "snippet": "snippet", + }, + { + "file_id": "file_id", + "snippet": "snippet", + }, + ], + }, + { + "answer": "answer", + "query": "query", + "sources": [ + { + "file_id": "file_id", + "snippet": "snippet", + }, + { + "file_id": "file_id", + "snippet": "snippet", + }, + { + "file_id": "file_id", + "snippet": "snippet", + }, + ], + }, + ], + }, "name": "name", + "refusal": "refusal", "tool_call_id": "tool_call_id", + "tool_calls": [ + { + "id": "id", + "function": { + "arguments": "arguments", + "name": "name", + }, + "type": "type", + "index": 0, + } + ], } ], model="palmyra-x-004", From 876d46d7b5af9217959a7c2b46f30521e1348343 Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Wed, 30 Oct 2024 20:43:41 +0000 Subject: [PATCH 100/399] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index daefc3e5..6c33430f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-94a38a74ceffcba2040259f19be51ccf509cb5b6560f094f5a2312a2ef1f78b1.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-1cc5cdee043b0cdaf8a02e3f4e2799f17a4b88dd843cf31eea423843f5bbd4b8.yml From 6fc883b81a210f189e441a52fab9fed981414685 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 30 Oct 2024 21:11:46 +0000 Subject: [PATCH 101/399] chore: rebuild project due to codegen change (#101) --- .stats.yml | 2 +- api.md | 36 +++ src/writerai/_client.py | 8 + src/writerai/resources/__init__.py | 14 + src/writerai/resources/tools/__init__.py | 47 ++++ src/writerai/resources/tools/medical.py | 194 +++++++++++++ src/writerai/resources/tools/pdf_parser.py | 178 ++++++++++++ src/writerai/resources/tools/tools.py | 260 ++++++++++++++++++ src/writerai/types/__init__.py | 4 + .../tool_context_aware_splitting_params.py | 19 ++ .../tool_context_aware_splitting_response.py | 15 + src/writerai/types/tools/__init__.py | 8 + .../types/tools/medical_create_params.py | 19 ++ .../types/tools/medical_create_response.py | 90 ++++++ .../types/tools/pdf_parser_parse_params.py | 12 + .../types/tools/pdf_parser_parse_response.py | 11 + tests/api_resources/test_tools.py | 90 ++++++ tests/api_resources/tools/__init__.py | 1 + tests/api_resources/tools/test_medical.py | 90 ++++++ tests/api_resources/tools/test_pdf_parser.py | 106 +++++++ 20 files changed, 1203 insertions(+), 1 deletion(-) create mode 100644 src/writerai/resources/tools/__init__.py create mode 100644 src/writerai/resources/tools/medical.py create mode 100644 src/writerai/resources/tools/pdf_parser.py create mode 100644 src/writerai/resources/tools/tools.py create mode 100644 src/writerai/types/tool_context_aware_splitting_params.py create mode 100644 src/writerai/types/tool_context_aware_splitting_response.py create mode 100644 src/writerai/types/tools/__init__.py create mode 100644 src/writerai/types/tools/medical_create_params.py create mode 100644 src/writerai/types/tools/medical_create_response.py create mode 100644 src/writerai/types/tools/pdf_parser_parse_params.py create mode 100644 src/writerai/types/tools/pdf_parser_parse_response.py create mode 100644 tests/api_resources/test_tools.py create mode 100644 tests/api_resources/tools/__init__.py create mode 100644 tests/api_resources/tools/test_medical.py create mode 100644 tests/api_resources/tools/test_pdf_parser.py diff --git a/.stats.yml b/.stats.yml index 6c33430f..6641d6d9 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ -configured_endpoints: 18 +configured_endpoints: 21 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-1cc5cdee043b0cdaf8a02e3f4e2799f17a4b88dd843cf31eea423843f5bbd4b8.yml diff --git a/api.md b/api.md index 40e6fd46..db3ab80f 100644 --- a/api.md +++ b/api.md @@ -88,3 +88,39 @@ Methods: - client.files.download(file_id) -> BinaryAPIResponse - client.files.retry(\*\*params) -> FileRetryResponse - client.files.upload(\*\*params) -> File + +# Tools + +Types: + +```python +from writerai.types import ToolContextAwareSplittingResponse +``` + +Methods: + +- client.tools.context_aware_splitting(\*\*params) -> ToolContextAwareSplittingResponse + +## Medical + +Types: + +```python +from writerai.types.tools import MedicalCreateResponse +``` + +Methods: + +- client.tools.medical.create(\*\*params) -> MedicalCreateResponse + +## PdfParser + +Types: + +```python +from writerai.types.tools import PdfParserParseResponse +``` + +Methods: + +- client.tools.pdf_parser.parse(file_id, \*\*params) -> PdfParserParseResponse diff --git a/src/writerai/_client.py b/src/writerai/_client.py index 120b9340..0cbecbb5 100644 --- a/src/writerai/_client.py +++ b/src/writerai/_client.py @@ -52,6 +52,7 @@ class Writer(SyncAPIClient): models: resources.ModelsResource graphs: resources.GraphsResource files: resources.FilesResource + tools: resources.ToolsResource with_raw_response: WriterWithRawResponse with_streaming_response: WriterWithStreamedResponse @@ -117,6 +118,7 @@ def __init__( self.models = resources.ModelsResource(self) self.graphs = resources.GraphsResource(self) self.files = resources.FilesResource(self) + self.tools = resources.ToolsResource(self) self.with_raw_response = WriterWithRawResponse(self) self.with_streaming_response = WriterWithStreamedResponse(self) @@ -232,6 +234,7 @@ class AsyncWriter(AsyncAPIClient): models: resources.AsyncModelsResource graphs: resources.AsyncGraphsResource files: resources.AsyncFilesResource + tools: resources.AsyncToolsResource with_raw_response: AsyncWriterWithRawResponse with_streaming_response: AsyncWriterWithStreamedResponse @@ -297,6 +300,7 @@ def __init__( self.models = resources.AsyncModelsResource(self) self.graphs = resources.AsyncGraphsResource(self) self.files = resources.AsyncFilesResource(self) + self.tools = resources.AsyncToolsResource(self) self.with_raw_response = AsyncWriterWithRawResponse(self) self.with_streaming_response = AsyncWriterWithStreamedResponse(self) @@ -413,6 +417,7 @@ def __init__(self, client: Writer) -> None: self.models = resources.ModelsResourceWithRawResponse(client.models) self.graphs = resources.GraphsResourceWithRawResponse(client.graphs) self.files = resources.FilesResourceWithRawResponse(client.files) + self.tools = resources.ToolsResourceWithRawResponse(client.tools) class AsyncWriterWithRawResponse: @@ -423,6 +428,7 @@ def __init__(self, client: AsyncWriter) -> None: self.models = resources.AsyncModelsResourceWithRawResponse(client.models) self.graphs = resources.AsyncGraphsResourceWithRawResponse(client.graphs) self.files = resources.AsyncFilesResourceWithRawResponse(client.files) + self.tools = resources.AsyncToolsResourceWithRawResponse(client.tools) class WriterWithStreamedResponse: @@ -433,6 +439,7 @@ def __init__(self, client: Writer) -> None: self.models = resources.ModelsResourceWithStreamingResponse(client.models) self.graphs = resources.GraphsResourceWithStreamingResponse(client.graphs) self.files = resources.FilesResourceWithStreamingResponse(client.files) + self.tools = resources.ToolsResourceWithStreamingResponse(client.tools) class AsyncWriterWithStreamedResponse: @@ -443,6 +450,7 @@ def __init__(self, client: AsyncWriter) -> None: self.models = resources.AsyncModelsResourceWithStreamingResponse(client.models) self.graphs = resources.AsyncGraphsResourceWithStreamingResponse(client.graphs) self.files = resources.AsyncFilesResourceWithStreamingResponse(client.files) + self.tools = resources.AsyncToolsResourceWithStreamingResponse(client.tools) Client = Writer diff --git a/src/writerai/resources/__init__.py b/src/writerai/resources/__init__.py index 739eb03b..fc4062c7 100644 --- a/src/writerai/resources/__init__.py +++ b/src/writerai/resources/__init__.py @@ -16,6 +16,14 @@ FilesResourceWithStreamingResponse, AsyncFilesResourceWithStreamingResponse, ) +from .tools import ( + ToolsResource, + AsyncToolsResource, + ToolsResourceWithRawResponse, + AsyncToolsResourceWithRawResponse, + ToolsResourceWithStreamingResponse, + AsyncToolsResourceWithStreamingResponse, +) from .graphs import ( GraphsResource, AsyncGraphsResource, @@ -86,4 +94,10 @@ "AsyncFilesResourceWithRawResponse", "FilesResourceWithStreamingResponse", "AsyncFilesResourceWithStreamingResponse", + "ToolsResource", + "AsyncToolsResource", + "ToolsResourceWithRawResponse", + "AsyncToolsResourceWithRawResponse", + "ToolsResourceWithStreamingResponse", + "AsyncToolsResourceWithStreamingResponse", ] diff --git a/src/writerai/resources/tools/__init__.py b/src/writerai/resources/tools/__init__.py new file mode 100644 index 00000000..277efbb8 --- /dev/null +++ b/src/writerai/resources/tools/__init__.py @@ -0,0 +1,47 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .tools import ( + ToolsResource, + AsyncToolsResource, + ToolsResourceWithRawResponse, + AsyncToolsResourceWithRawResponse, + ToolsResourceWithStreamingResponse, + AsyncToolsResourceWithStreamingResponse, +) +from .medical import ( + MedicalResource, + AsyncMedicalResource, + MedicalResourceWithRawResponse, + AsyncMedicalResourceWithRawResponse, + MedicalResourceWithStreamingResponse, + AsyncMedicalResourceWithStreamingResponse, +) +from .pdf_parser import ( + PdfParserResource, + AsyncPdfParserResource, + PdfParserResourceWithRawResponse, + AsyncPdfParserResourceWithRawResponse, + PdfParserResourceWithStreamingResponse, + AsyncPdfParserResourceWithStreamingResponse, +) + +__all__ = [ + "MedicalResource", + "AsyncMedicalResource", + "MedicalResourceWithRawResponse", + "AsyncMedicalResourceWithRawResponse", + "MedicalResourceWithStreamingResponse", + "AsyncMedicalResourceWithStreamingResponse", + "PdfParserResource", + "AsyncPdfParserResource", + "PdfParserResourceWithRawResponse", + "AsyncPdfParserResourceWithRawResponse", + "PdfParserResourceWithStreamingResponse", + "AsyncPdfParserResourceWithStreamingResponse", + "ToolsResource", + "AsyncToolsResource", + "ToolsResourceWithRawResponse", + "AsyncToolsResourceWithRawResponse", + "ToolsResourceWithStreamingResponse", + "AsyncToolsResourceWithStreamingResponse", +] diff --git a/src/writerai/resources/tools/medical.py b/src/writerai/resources/tools/medical.py new file mode 100644 index 00000000..e3b10e22 --- /dev/null +++ b/src/writerai/resources/tools/medical.py @@ -0,0 +1,194 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal + +import httpx + +from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ..._utils import ( + maybe_transform, + async_maybe_transform, +) +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ...types.tools import medical_create_params +from ..._base_client import make_request_options +from ...types.tools.medical_create_response import MedicalCreateResponse + +__all__ = ["MedicalResource", "AsyncMedicalResource"] + + +class MedicalResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> MedicalResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers + """ + return MedicalResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> MedicalResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/writer/writer-python#with_streaming_response + """ + return MedicalResourceWithStreamingResponse(self) + + def create( + self, + *, + content: str, + response_type: Literal["Entities", "RxNorm", "ICD-10-CM", "SNOMED CT"], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> MedicalCreateResponse: + """ + Create a completion using Palmyra medical model. + + Args: + content: The text to be analyzed. + + response_type: The structure of the response to be returned. `Entities` returns medical + entities, `RxNorm` returns medication information, `ICD-10-CM` returns diagnosis + codes, and `SNOMED CT` returns medical concepts. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/v1/tools/comprehend/medical", + body=maybe_transform( + { + "content": content, + "response_type": response_type, + }, + medical_create_params.MedicalCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MedicalCreateResponse, + ) + + +class AsyncMedicalResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncMedicalResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers + """ + return AsyncMedicalResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncMedicalResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/writer/writer-python#with_streaming_response + """ + return AsyncMedicalResourceWithStreamingResponse(self) + + async def create( + self, + *, + content: str, + response_type: Literal["Entities", "RxNorm", "ICD-10-CM", "SNOMED CT"], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> MedicalCreateResponse: + """ + Create a completion using Palmyra medical model. + + Args: + content: The text to be analyzed. + + response_type: The structure of the response to be returned. `Entities` returns medical + entities, `RxNorm` returns medication information, `ICD-10-CM` returns diagnosis + codes, and `SNOMED CT` returns medical concepts. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/v1/tools/comprehend/medical", + body=await async_maybe_transform( + { + "content": content, + "response_type": response_type, + }, + medical_create_params.MedicalCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MedicalCreateResponse, + ) + + +class MedicalResourceWithRawResponse: + def __init__(self, medical: MedicalResource) -> None: + self._medical = medical + + self.create = to_raw_response_wrapper( + medical.create, + ) + + +class AsyncMedicalResourceWithRawResponse: + def __init__(self, medical: AsyncMedicalResource) -> None: + self._medical = medical + + self.create = async_to_raw_response_wrapper( + medical.create, + ) + + +class MedicalResourceWithStreamingResponse: + def __init__(self, medical: MedicalResource) -> None: + self._medical = medical + + self.create = to_streamed_response_wrapper( + medical.create, + ) + + +class AsyncMedicalResourceWithStreamingResponse: + def __init__(self, medical: AsyncMedicalResource) -> None: + self._medical = medical + + self.create = async_to_streamed_response_wrapper( + medical.create, + ) diff --git a/src/writerai/resources/tools/pdf_parser.py b/src/writerai/resources/tools/pdf_parser.py new file mode 100644 index 00000000..6b9cd379 --- /dev/null +++ b/src/writerai/resources/tools/pdf_parser.py @@ -0,0 +1,178 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal + +import httpx + +from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ..._utils import ( + maybe_transform, + async_maybe_transform, +) +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ...types.tools import pdf_parser_parse_params +from ..._base_client import make_request_options +from ...types.tools.pdf_parser_parse_response import PdfParserParseResponse + +__all__ = ["PdfParserResource", "AsyncPdfParserResource"] + + +class PdfParserResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> PdfParserResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers + """ + return PdfParserResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> PdfParserResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/writer/writer-python#with_streaming_response + """ + return PdfParserResourceWithStreamingResponse(self) + + def parse( + self, + file_id: str, + *, + format: Literal["text", "markdown"], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> PdfParserParseResponse: + """ + Parse PDF to other formats. + + Args: + format: The format into which the PDF content should be converted. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + return self._post( + f"/v1/tools/pdf-parser/{file_id}", + body=maybe_transform({"format": format}, pdf_parser_parse_params.PdfParserParseParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=PdfParserParseResponse, + ) + + +class AsyncPdfParserResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncPdfParserResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers + """ + return AsyncPdfParserResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncPdfParserResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/writer/writer-python#with_streaming_response + """ + return AsyncPdfParserResourceWithStreamingResponse(self) + + async def parse( + self, + file_id: str, + *, + format: Literal["text", "markdown"], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> PdfParserParseResponse: + """ + Parse PDF to other formats. + + Args: + format: The format into which the PDF content should be converted. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + return await self._post( + f"/v1/tools/pdf-parser/{file_id}", + body=await async_maybe_transform({"format": format}, pdf_parser_parse_params.PdfParserParseParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=PdfParserParseResponse, + ) + + +class PdfParserResourceWithRawResponse: + def __init__(self, pdf_parser: PdfParserResource) -> None: + self._pdf_parser = pdf_parser + + self.parse = to_raw_response_wrapper( + pdf_parser.parse, + ) + + +class AsyncPdfParserResourceWithRawResponse: + def __init__(self, pdf_parser: AsyncPdfParserResource) -> None: + self._pdf_parser = pdf_parser + + self.parse = async_to_raw_response_wrapper( + pdf_parser.parse, + ) + + +class PdfParserResourceWithStreamingResponse: + def __init__(self, pdf_parser: PdfParserResource) -> None: + self._pdf_parser = pdf_parser + + self.parse = to_streamed_response_wrapper( + pdf_parser.parse, + ) + + +class AsyncPdfParserResourceWithStreamingResponse: + def __init__(self, pdf_parser: AsyncPdfParserResource) -> None: + self._pdf_parser = pdf_parser + + self.parse = async_to_streamed_response_wrapper( + pdf_parser.parse, + ) diff --git a/src/writerai/resources/tools/tools.py b/src/writerai/resources/tools/tools.py new file mode 100644 index 00000000..d8e3d934 --- /dev/null +++ b/src/writerai/resources/tools/tools.py @@ -0,0 +1,260 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal + +import httpx + +from ...types import tool_context_aware_splitting_params +from .medical import ( + MedicalResource, + AsyncMedicalResource, + MedicalResourceWithRawResponse, + AsyncMedicalResourceWithRawResponse, + MedicalResourceWithStreamingResponse, + AsyncMedicalResourceWithStreamingResponse, +) +from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ..._utils import ( + maybe_transform, + async_maybe_transform, +) +from ..._compat import cached_property +from .pdf_parser import ( + PdfParserResource, + AsyncPdfParserResource, + PdfParserResourceWithRawResponse, + AsyncPdfParserResourceWithRawResponse, + PdfParserResourceWithStreamingResponse, + AsyncPdfParserResourceWithStreamingResponse, +) +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..._base_client import make_request_options +from ...types.tool_context_aware_splitting_response import ToolContextAwareSplittingResponse + +__all__ = ["ToolsResource", "AsyncToolsResource"] + + +class ToolsResource(SyncAPIResource): + @cached_property + def medical(self) -> MedicalResource: + return MedicalResource(self._client) + + @cached_property + def pdf_parser(self) -> PdfParserResource: + return PdfParserResource(self._client) + + @cached_property + def with_raw_response(self) -> ToolsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers + """ + return ToolsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ToolsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/writer/writer-python#with_streaming_response + """ + return ToolsResourceWithStreamingResponse(self) + + def context_aware_splitting( + self, + *, + strategy: Literal["llm_split", "fast_split", "hybrid_split"], + text: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ToolContextAwareSplittingResponse: + """ + Splits a long block of text (maximum 4000 words) into smaller chunks while + preserving the semantic meaning of the text and context between the chunks. + + Args: + strategy: The strategy to be used for splitting the text into chunks. `llm_split` uses the + language model to split the text, `fast_split` uses a fast heuristic-based + approach, and `hybrid_split` combines both strategies. + + text: The text to be split into chunks. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/v1/tools/context-aware-splitting", + body=maybe_transform( + { + "strategy": strategy, + "text": text, + }, + tool_context_aware_splitting_params.ToolContextAwareSplittingParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ToolContextAwareSplittingResponse, + ) + + +class AsyncToolsResource(AsyncAPIResource): + @cached_property + def medical(self) -> AsyncMedicalResource: + return AsyncMedicalResource(self._client) + + @cached_property + def pdf_parser(self) -> AsyncPdfParserResource: + return AsyncPdfParserResource(self._client) + + @cached_property + def with_raw_response(self) -> AsyncToolsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers + """ + return AsyncToolsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncToolsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/writer/writer-python#with_streaming_response + """ + return AsyncToolsResourceWithStreamingResponse(self) + + async def context_aware_splitting( + self, + *, + strategy: Literal["llm_split", "fast_split", "hybrid_split"], + text: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ToolContextAwareSplittingResponse: + """ + Splits a long block of text (maximum 4000 words) into smaller chunks while + preserving the semantic meaning of the text and context between the chunks. + + Args: + strategy: The strategy to be used for splitting the text into chunks. `llm_split` uses the + language model to split the text, `fast_split` uses a fast heuristic-based + approach, and `hybrid_split` combines both strategies. + + text: The text to be split into chunks. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/v1/tools/context-aware-splitting", + body=await async_maybe_transform( + { + "strategy": strategy, + "text": text, + }, + tool_context_aware_splitting_params.ToolContextAwareSplittingParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ToolContextAwareSplittingResponse, + ) + + +class ToolsResourceWithRawResponse: + def __init__(self, tools: ToolsResource) -> None: + self._tools = tools + + self.context_aware_splitting = to_raw_response_wrapper( + tools.context_aware_splitting, + ) + + @cached_property + def medical(self) -> MedicalResourceWithRawResponse: + return MedicalResourceWithRawResponse(self._tools.medical) + + @cached_property + def pdf_parser(self) -> PdfParserResourceWithRawResponse: + return PdfParserResourceWithRawResponse(self._tools.pdf_parser) + + +class AsyncToolsResourceWithRawResponse: + def __init__(self, tools: AsyncToolsResource) -> None: + self._tools = tools + + self.context_aware_splitting = async_to_raw_response_wrapper( + tools.context_aware_splitting, + ) + + @cached_property + def medical(self) -> AsyncMedicalResourceWithRawResponse: + return AsyncMedicalResourceWithRawResponse(self._tools.medical) + + @cached_property + def pdf_parser(self) -> AsyncPdfParserResourceWithRawResponse: + return AsyncPdfParserResourceWithRawResponse(self._tools.pdf_parser) + + +class ToolsResourceWithStreamingResponse: + def __init__(self, tools: ToolsResource) -> None: + self._tools = tools + + self.context_aware_splitting = to_streamed_response_wrapper( + tools.context_aware_splitting, + ) + + @cached_property + def medical(self) -> MedicalResourceWithStreamingResponse: + return MedicalResourceWithStreamingResponse(self._tools.medical) + + @cached_property + def pdf_parser(self) -> PdfParserResourceWithStreamingResponse: + return PdfParserResourceWithStreamingResponse(self._tools.pdf_parser) + + +class AsyncToolsResourceWithStreamingResponse: + def __init__(self, tools: AsyncToolsResource) -> None: + self._tools = tools + + self.context_aware_splitting = async_to_streamed_response_wrapper( + tools.context_aware_splitting, + ) + + @cached_property + def medical(self) -> AsyncMedicalResourceWithStreamingResponse: + return AsyncMedicalResourceWithStreamingResponse(self._tools.medical) + + @cached_property + def pdf_parser(self) -> AsyncPdfParserResourceWithStreamingResponse: + return AsyncPdfParserResourceWithStreamingResponse(self._tools.pdf_parser) diff --git a/src/writerai/types/__init__.py b/src/writerai/types/__init__.py index 95ddde62..6646f0fb 100644 --- a/src/writerai/types/__init__.py +++ b/src/writerai/types/__init__.py @@ -26,7 +26,11 @@ from .completion_create_params import CompletionCreateParams as CompletionCreateParams from .graph_add_file_to_graph_params import GraphAddFileToGraphParams as GraphAddFileToGraphParams from .application_generate_content_params import ApplicationGenerateContentParams as ApplicationGenerateContentParams +from .tool_context_aware_splitting_params import ToolContextAwareSplittingParams as ToolContextAwareSplittingParams from .application_generate_content_response import ( ApplicationGenerateContentResponse as ApplicationGenerateContentResponse, ) from .graph_remove_file_from_graph_response import GraphRemoveFileFromGraphResponse as GraphRemoveFileFromGraphResponse +from .tool_context_aware_splitting_response import ( + ToolContextAwareSplittingResponse as ToolContextAwareSplittingResponse, +) diff --git a/src/writerai/types/tool_context_aware_splitting_params.py b/src/writerai/types/tool_context_aware_splitting_params.py new file mode 100644 index 00000000..b9ad4b06 --- /dev/null +++ b/src/writerai/types/tool_context_aware_splitting_params.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["ToolContextAwareSplittingParams"] + + +class ToolContextAwareSplittingParams(TypedDict, total=False): + strategy: Required[Literal["llm_split", "fast_split", "hybrid_split"]] + """The strategy to be used for splitting the text into chunks. + + `llm_split` uses the language model to split the text, `fast_split` uses a fast + heuristic-based approach, and `hybrid_split` combines both strategies. + """ + + text: Required[str] + """The text to be split into chunks.""" diff --git a/src/writerai/types/tool_context_aware_splitting_response.py b/src/writerai/types/tool_context_aware_splitting_response.py new file mode 100644 index 00000000..74f3a773 --- /dev/null +++ b/src/writerai/types/tool_context_aware_splitting_response.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List + +from .._models import BaseModel + +__all__ = ["ToolContextAwareSplittingResponse"] + + +class ToolContextAwareSplittingResponse(BaseModel): + chunks: List[str] + """ + An array of text chunks generated by splitting the input text based on the + specified strategy. + """ diff --git a/src/writerai/types/tools/__init__.py b/src/writerai/types/tools/__init__.py new file mode 100644 index 00000000..fd852e5e --- /dev/null +++ b/src/writerai/types/tools/__init__.py @@ -0,0 +1,8 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .medical_create_params import MedicalCreateParams as MedicalCreateParams +from .medical_create_response import MedicalCreateResponse as MedicalCreateResponse +from .pdf_parser_parse_params import PdfParserParseParams as PdfParserParseParams +from .pdf_parser_parse_response import PdfParserParseResponse as PdfParserParseResponse diff --git a/src/writerai/types/tools/medical_create_params.py b/src/writerai/types/tools/medical_create_params.py new file mode 100644 index 00000000..3c15f70a --- /dev/null +++ b/src/writerai/types/tools/medical_create_params.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["MedicalCreateParams"] + + +class MedicalCreateParams(TypedDict, total=False): + content: Required[str] + """The text to be analyzed.""" + + response_type: Required[Literal["Entities", "RxNorm", "ICD-10-CM", "SNOMED CT"]] + """The structure of the response to be returned. + + `Entities` returns medical entities, `RxNorm` returns medication information, + `ICD-10-CM` returns diagnosis codes, and `SNOMED CT` returns medical concepts. + """ diff --git a/src/writerai/types/tools/medical_create_response.py b/src/writerai/types/tools/medical_create_response.py new file mode 100644 index 00000000..28ebd094 --- /dev/null +++ b/src/writerai/types/tools/medical_create_response.py @@ -0,0 +1,90 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from ..._models import BaseModel + +__all__ = [ + "MedicalCreateResponse", + "Entity", + "EntityAttribute", + "EntityAttributeConcept", + "EntityAttributeTrait", + "EntityConcept", + "EntityTrait", +] + + +class EntityAttributeConcept(BaseModel): + code: str + + description: str + + score: float + + +class EntityAttributeTrait(BaseModel): + name: str + + score: float + + +class EntityAttribute(BaseModel): + begin_offset: int + + concepts: List[EntityAttributeConcept] + + end_offset: int + + relationship_score: float + + score: float + + text: str + + traits: List[EntityAttributeTrait] + + type: str + + category: Optional[str] = None + + relationship_type: Optional[str] = None + + +class EntityConcept(BaseModel): + code: str + + description: str + + score: float + + +class EntityTrait(BaseModel): + name: str + + score: float + + +class Entity(BaseModel): + attributes: List[EntityAttribute] + + begin_offset: int + + category: str + + concepts: List[EntityConcept] + + end_offset: int + + score: float + + text: str + + traits: List[EntityTrait] + + type: str + + +class MedicalCreateResponse(BaseModel): + entities: List[Entity] + """An array of medical entities extracted from the input text.""" diff --git a/src/writerai/types/tools/pdf_parser_parse_params.py b/src/writerai/types/tools/pdf_parser_parse_params.py new file mode 100644 index 00000000..5cbb30e2 --- /dev/null +++ b/src/writerai/types/tools/pdf_parser_parse_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["PdfParserParseParams"] + + +class PdfParserParseParams(TypedDict, total=False): + format: Required[Literal["text", "markdown"]] + """The format into which the PDF content should be converted.""" diff --git a/src/writerai/types/tools/pdf_parser_parse_response.py b/src/writerai/types/tools/pdf_parser_parse_response.py new file mode 100644 index 00000000..15390b01 --- /dev/null +++ b/src/writerai/types/tools/pdf_parser_parse_response.py @@ -0,0 +1,11 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + + +from ..._models import BaseModel + +__all__ = ["PdfParserParseResponse"] + + +class PdfParserParseResponse(BaseModel): + content: str + """The extracted content from the PDF file, converted to the specified format.""" diff --git a/tests/api_resources/test_tools.py b/tests/api_resources/test_tools.py new file mode 100644 index 00000000..b5d8b828 --- /dev/null +++ b/tests/api_resources/test_tools.py @@ -0,0 +1,90 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from writerai import Writer, AsyncWriter +from tests.utils import assert_matches_type +from writerai.types import ToolContextAwareSplittingResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestTools: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_context_aware_splitting(self, client: Writer) -> None: + tool = client.tools.context_aware_splitting( + strategy="llm_split", + text="text", + ) + assert_matches_type(ToolContextAwareSplittingResponse, tool, path=["response"]) + + @parametrize + def test_raw_response_context_aware_splitting(self, client: Writer) -> None: + response = client.tools.with_raw_response.context_aware_splitting( + strategy="llm_split", + text="text", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + tool = response.parse() + assert_matches_type(ToolContextAwareSplittingResponse, tool, path=["response"]) + + @parametrize + def test_streaming_response_context_aware_splitting(self, client: Writer) -> None: + with client.tools.with_streaming_response.context_aware_splitting( + strategy="llm_split", + text="text", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + tool = response.parse() + assert_matches_type(ToolContextAwareSplittingResponse, tool, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncTools: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_context_aware_splitting(self, async_client: AsyncWriter) -> None: + tool = await async_client.tools.context_aware_splitting( + strategy="llm_split", + text="text", + ) + assert_matches_type(ToolContextAwareSplittingResponse, tool, path=["response"]) + + @parametrize + async def test_raw_response_context_aware_splitting(self, async_client: AsyncWriter) -> None: + response = await async_client.tools.with_raw_response.context_aware_splitting( + strategy="llm_split", + text="text", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + tool = await response.parse() + assert_matches_type(ToolContextAwareSplittingResponse, tool, path=["response"]) + + @parametrize + async def test_streaming_response_context_aware_splitting(self, async_client: AsyncWriter) -> None: + async with async_client.tools.with_streaming_response.context_aware_splitting( + strategy="llm_split", + text="text", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + tool = await response.parse() + assert_matches_type(ToolContextAwareSplittingResponse, tool, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/tools/__init__.py b/tests/api_resources/tools/__init__.py new file mode 100644 index 00000000..fd8019a9 --- /dev/null +++ b/tests/api_resources/tools/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/tools/test_medical.py b/tests/api_resources/tools/test_medical.py new file mode 100644 index 00000000..c2592b2e --- /dev/null +++ b/tests/api_resources/tools/test_medical.py @@ -0,0 +1,90 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from writerai import Writer, AsyncWriter +from tests.utils import assert_matches_type +from writerai.types.tools import MedicalCreateResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestMedical: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: Writer) -> None: + medical = client.tools.medical.create( + content="content", + response_type="Entities", + ) + assert_matches_type(MedicalCreateResponse, medical, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Writer) -> None: + response = client.tools.medical.with_raw_response.create( + content="content", + response_type="Entities", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + medical = response.parse() + assert_matches_type(MedicalCreateResponse, medical, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Writer) -> None: + with client.tools.medical.with_streaming_response.create( + content="content", + response_type="Entities", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + medical = response.parse() + assert_matches_type(MedicalCreateResponse, medical, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncMedical: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_create(self, async_client: AsyncWriter) -> None: + medical = await async_client.tools.medical.create( + content="content", + response_type="Entities", + ) + assert_matches_type(MedicalCreateResponse, medical, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncWriter) -> None: + response = await async_client.tools.medical.with_raw_response.create( + content="content", + response_type="Entities", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + medical = await response.parse() + assert_matches_type(MedicalCreateResponse, medical, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncWriter) -> None: + async with async_client.tools.medical.with_streaming_response.create( + content="content", + response_type="Entities", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + medical = await response.parse() + assert_matches_type(MedicalCreateResponse, medical, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/tools/test_pdf_parser.py b/tests/api_resources/tools/test_pdf_parser.py new file mode 100644 index 00000000..5d711a3e --- /dev/null +++ b/tests/api_resources/tools/test_pdf_parser.py @@ -0,0 +1,106 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from writerai import Writer, AsyncWriter +from tests.utils import assert_matches_type +from writerai.types.tools import PdfParserParseResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestPdfParser: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_parse(self, client: Writer) -> None: + pdf_parser = client.tools.pdf_parser.parse( + file_id="file_id", + format="text", + ) + assert_matches_type(PdfParserParseResponse, pdf_parser, path=["response"]) + + @parametrize + def test_raw_response_parse(self, client: Writer) -> None: + response = client.tools.pdf_parser.with_raw_response.parse( + file_id="file_id", + format="text", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + pdf_parser = response.parse() + assert_matches_type(PdfParserParseResponse, pdf_parser, path=["response"]) + + @parametrize + def test_streaming_response_parse(self, client: Writer) -> None: + with client.tools.pdf_parser.with_streaming_response.parse( + file_id="file_id", + format="text", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + pdf_parser = response.parse() + assert_matches_type(PdfParserParseResponse, pdf_parser, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_parse(self, client: Writer) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): + client.tools.pdf_parser.with_raw_response.parse( + file_id="", + format="text", + ) + + +class TestAsyncPdfParser: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_parse(self, async_client: AsyncWriter) -> None: + pdf_parser = await async_client.tools.pdf_parser.parse( + file_id="file_id", + format="text", + ) + assert_matches_type(PdfParserParseResponse, pdf_parser, path=["response"]) + + @parametrize + async def test_raw_response_parse(self, async_client: AsyncWriter) -> None: + response = await async_client.tools.pdf_parser.with_raw_response.parse( + file_id="file_id", + format="text", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + pdf_parser = await response.parse() + assert_matches_type(PdfParserParseResponse, pdf_parser, path=["response"]) + + @parametrize + async def test_streaming_response_parse(self, async_client: AsyncWriter) -> None: + async with async_client.tools.pdf_parser.with_streaming_response.parse( + file_id="file_id", + format="text", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + pdf_parser = await response.parse() + assert_matches_type(PdfParserParseResponse, pdf_parser, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_parse(self, async_client: AsyncWriter) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): + await async_client.tools.pdf_parser.with_raw_response.parse( + file_id="", + format="text", + ) From 28f8d7d61f042cc27985f23c2a5fbc5e10c8b424 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 30 Oct 2024 22:56:00 +0000 Subject: [PATCH 102/399] feat(api): manual updates (#102) --- .stats.yml | 2 +- src/writerai/types/chat_chat_params.py | 6 +++--- tests/api_resources/test_chat.py | 24 ++++++++++++------------ 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.stats.yml b/.stats.yml index 6641d6d9..fc7fd561 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-1cc5cdee043b0cdaf8a02e3f4e2799f17a4b88dd843cf31eea423843f5bbd4b8.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-8e1a6a7a707502c2d026f1201825d0ce91e7a8f71d74cf6eebc95991b48be760.yml diff --git a/src/writerai/types/chat_chat_params.py b/src/writerai/types/chat_chat_params.py index 0b453e91..4bf0acd7 100644 --- a/src/writerai/types/chat_chat_params.py +++ b/src/writerai/types/chat_chat_params.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import List, Union, Iterable, Optional +from typing import Dict, List, Union, Iterable, Optional from typing_extensions import Literal, Required, TypeAlias, TypedDict __all__ = [ @@ -174,7 +174,7 @@ class ToolChoiceStringToolChoice(TypedDict, total=False): class ToolChoiceJsonObjectToolChoice(TypedDict, total=False): - value: Required[object] + value: Required[Dict[str, object]] ToolChoice: TypeAlias = Union[ToolChoiceStringToolChoice, ToolChoiceJsonObjectToolChoice] @@ -187,7 +187,7 @@ class ToolFunctionToolFunction(TypedDict, total=False): description: str """Description of the function""" - parameters: object + parameters: Dict[str, object] class ToolFunctionTool(TypedDict, total=False): diff --git a/tests/api_resources/test_chat.py b/tests/api_resources/test_chat.py index 39338d19..0906891a 100644 --- a/tests/api_resources/test_chat.py +++ b/tests/api_resources/test_chat.py @@ -135,7 +135,7 @@ def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: "function": { "name": "name", "description": "description", - "parameters": {}, + "parameters": {"foo": "bar"}, }, "type": "function", }, @@ -143,7 +143,7 @@ def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: "function": { "name": "name", "description": "description", - "parameters": {}, + "parameters": {"foo": "bar"}, }, "type": "function", }, @@ -151,7 +151,7 @@ def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: "function": { "name": "name", "description": "description", - "parameters": {}, + "parameters": {"foo": "bar"}, }, "type": "function", }, @@ -305,7 +305,7 @@ def test_method_chat_with_all_params_overload_2(self, client: Writer) -> None: "function": { "name": "name", "description": "description", - "parameters": {}, + "parameters": {"foo": "bar"}, }, "type": "function", }, @@ -313,7 +313,7 @@ def test_method_chat_with_all_params_overload_2(self, client: Writer) -> None: "function": { "name": "name", "description": "description", - "parameters": {}, + "parameters": {"foo": "bar"}, }, "type": "function", }, @@ -321,7 +321,7 @@ def test_method_chat_with_all_params_overload_2(self, client: Writer) -> None: "function": { "name": "name", "description": "description", - "parameters": {}, + "parameters": {"foo": "bar"}, }, "type": "function", }, @@ -479,7 +479,7 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW "function": { "name": "name", "description": "description", - "parameters": {}, + "parameters": {"foo": "bar"}, }, "type": "function", }, @@ -487,7 +487,7 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW "function": { "name": "name", "description": "description", - "parameters": {}, + "parameters": {"foo": "bar"}, }, "type": "function", }, @@ -495,7 +495,7 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW "function": { "name": "name", "description": "description", - "parameters": {}, + "parameters": {"foo": "bar"}, }, "type": "function", }, @@ -649,7 +649,7 @@ async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncW "function": { "name": "name", "description": "description", - "parameters": {}, + "parameters": {"foo": "bar"}, }, "type": "function", }, @@ -657,7 +657,7 @@ async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncW "function": { "name": "name", "description": "description", - "parameters": {}, + "parameters": {"foo": "bar"}, }, "type": "function", }, @@ -665,7 +665,7 @@ async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncW "function": { "name": "name", "description": "description", - "parameters": {}, + "parameters": {"foo": "bar"}, }, "type": "function", }, From 3e2619755cb552e7bcce75be5667314dfef363b9 Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Wed, 30 Oct 2024 22:57:05 +0000 Subject: [PATCH 103/399] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index fc7fd561..8575778c 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-8e1a6a7a707502c2d026f1201825d0ce91e7a8f71d74cf6eebc95991b48be760.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-919924114cb7a4f2c6ee7633fb14e2864ede699a372b6819b97b9412128b52f5.yml From 0b8924d891fc4d35c822a1f04bfea67207e8fde8 Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Wed, 30 Oct 2024 23:00:47 +0000 Subject: [PATCH 104/399] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 8575778c..3da40098 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-919924114cb7a4f2c6ee7633fb14e2864ede699a372b6819b97b9412128b52f5.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-42c8a9982452241feac7fc5461b007247f7db7d9493f104b8bfb457a292365ac.yml From d405197d6f9120bdd881e070dc8859f5a57f53a2 Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Wed, 30 Oct 2024 23:17:19 +0000 Subject: [PATCH 105/399] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 3da40098..1c1c3c05 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-42c8a9982452241feac7fc5461b007247f7db7d9493f104b8bfb457a292365ac.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-d13c05cc3a960ab336c11a4e7a4f31f10cdf4b9aee968c8f8534145e39fb374a.yml From f90efd27dc01a6485aa01204d19e1a86195560a3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 1 Nov 2024 14:57:15 +0000 Subject: [PATCH 106/399] chore: rebuild project due to codegen change (#103) --- requirements-dev.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.lock b/requirements-dev.lock index eef2556c..364ec7fc 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -48,7 +48,7 @@ markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -mypy==1.11.2 +mypy==1.13.0 mypy-extensions==1.0.0 # via mypy nodeenv==1.8.0 From a3689e4d32b2db3315ab7303a83c8fa74062703c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 2 Nov 2024 03:17:20 +0000 Subject: [PATCH 107/399] chore: rebuild project due to codegen change (#104) --- src/writerai/_utils/_transform.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/writerai/_utils/_transform.py b/src/writerai/_utils/_transform.py index 47e262a5..7e9663d3 100644 --- a/src/writerai/_utils/_transform.py +++ b/src/writerai/_utils/_transform.py @@ -173,6 +173,11 @@ def _transform_recursive( # Iterable[T] or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str)) ): + # dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually + # intended as an iterable, so we don't transform it. + if isinstance(data, dict): + return cast(object, data) + inner_type = extract_type_arg(stripped_type, 0) return [_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data] From 5619e5ad9933504604f93fe655fc00bda99e4410 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 15:41:45 +0000 Subject: [PATCH 108/399] chore: rebuild project due to codegen change (#105) --- src/writerai/_compat.py | 6 ++++-- src/writerai/_models.py | 9 ++++++--- src/writerai/_utils/__init__.py | 1 + src/writerai/_utils/_transform.py | 4 ++-- src/writerai/_utils/_utils.py | 17 +++++++++++++++++ tests/test_models.py | 21 +++++++-------------- tests/test_transform.py | 15 +++++++++++++++ 7 files changed, 52 insertions(+), 21 deletions(-) diff --git a/src/writerai/_compat.py b/src/writerai/_compat.py index d89920d9..4794129c 100644 --- a/src/writerai/_compat.py +++ b/src/writerai/_compat.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload from datetime import date, datetime -from typing_extensions import Self +from typing_extensions import Self, Literal import pydantic from pydantic.fields import FieldInfo @@ -137,9 +137,11 @@ def model_dump( exclude_unset: bool = False, exclude_defaults: bool = False, warnings: bool = True, + mode: Literal["json", "python"] = "python", ) -> dict[str, Any]: - if PYDANTIC_V2: + if PYDANTIC_V2 or hasattr(model, "model_dump"): return model.model_dump( + mode=mode, exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, diff --git a/src/writerai/_models.py b/src/writerai/_models.py index 42551b76..6cb469e2 100644 --- a/src/writerai/_models.py +++ b/src/writerai/_models.py @@ -37,6 +37,7 @@ PropertyInfo, is_list, is_given, + json_safe, lru_cache, is_mapping, parse_date, @@ -279,8 +280,8 @@ def model_dump( Returns: A dictionary representation of the model. """ - if mode != "python": - raise ValueError("mode is only supported in Pydantic v2") + if mode not in {"json", "python"}: + raise ValueError("mode must be either 'json' or 'python'") if round_trip != False: raise ValueError("round_trip is only supported in Pydantic v2") if warnings != True: @@ -289,7 +290,7 @@ def model_dump( raise ValueError("context is only supported in Pydantic v2") if serialize_as_any != False: raise ValueError("serialize_as_any is only supported in Pydantic v2") - return super().dict( # pyright: ignore[reportDeprecated] + dumped = super().dict( # pyright: ignore[reportDeprecated] include=include, exclude=exclude, by_alias=by_alias, @@ -298,6 +299,8 @@ def model_dump( exclude_none=exclude_none, ) + return cast(dict[str, Any], json_safe(dumped)) if mode == "json" else dumped + @override def model_dump_json( self, diff --git a/src/writerai/_utils/__init__.py b/src/writerai/_utils/__init__.py index 3efe66c8..a7cff3c0 100644 --- a/src/writerai/_utils/__init__.py +++ b/src/writerai/_utils/__init__.py @@ -6,6 +6,7 @@ is_list as is_list, is_given as is_given, is_tuple as is_tuple, + json_safe as json_safe, lru_cache as lru_cache, is_mapping as is_mapping, is_tuple_t as is_tuple_t, diff --git a/src/writerai/_utils/_transform.py b/src/writerai/_utils/_transform.py index 7e9663d3..d7c05345 100644 --- a/src/writerai/_utils/_transform.py +++ b/src/writerai/_utils/_transform.py @@ -191,7 +191,7 @@ def _transform_recursive( return data if isinstance(data, pydantic.BaseModel): - return model_dump(data, exclude_unset=True) + return model_dump(data, exclude_unset=True, mode="json") annotated_type = _get_annotated_type(annotation) if annotated_type is None: @@ -329,7 +329,7 @@ async def _async_transform_recursive( return data if isinstance(data, pydantic.BaseModel): - return model_dump(data, exclude_unset=True) + return model_dump(data, exclude_unset=True, mode="json") annotated_type = _get_annotated_type(annotation) if annotated_type is None: diff --git a/src/writerai/_utils/_utils.py b/src/writerai/_utils/_utils.py index 0bba17ca..e5811bba 100644 --- a/src/writerai/_utils/_utils.py +++ b/src/writerai/_utils/_utils.py @@ -16,6 +16,7 @@ overload, ) from pathlib import Path +from datetime import date, datetime from typing_extensions import TypeGuard import sniffio @@ -395,3 +396,19 @@ def lru_cache(*, maxsize: int | None = 128) -> Callable[[CallableT], CallableT]: maxsize=maxsize, ) return cast(Any, wrapper) # type: ignore[no-any-return] + + +def json_safe(data: object) -> object: + """Translates a mapping / sequence recursively in the same fashion + as `pydantic` v2's `model_dump(mode="json")`. + """ + if is_mapping(data): + return {json_safe(key): json_safe(value) for key, value in data.items()} + + if is_iterable(data) and not isinstance(data, (str, bytes, bytearray)): + return [json_safe(item) for item in data] + + if isinstance(data, (datetime, date)): + return data.isoformat() + + return data diff --git a/tests/test_models.py b/tests/test_models.py index 61c86a0b..6c746aee 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -520,19 +520,15 @@ class Model(BaseModel): assert m3.to_dict(exclude_none=True) == {} assert m3.to_dict(exclude_defaults=True) == {} - if PYDANTIC_V2: - - class Model2(BaseModel): - created_at: datetime + class Model2(BaseModel): + created_at: datetime - time_str = "2024-03-21T11:39:01.275859" - m4 = Model2.construct(created_at=time_str) - assert m4.to_dict(mode="python") == {"created_at": datetime.fromisoformat(time_str)} - assert m4.to_dict(mode="json") == {"created_at": time_str} - else: - with pytest.raises(ValueError, match="mode is only supported in Pydantic v2"): - m.to_dict(mode="json") + time_str = "2024-03-21T11:39:01.275859" + m4 = Model2.construct(created_at=time_str) + assert m4.to_dict(mode="python") == {"created_at": datetime.fromisoformat(time_str)} + assert m4.to_dict(mode="json") == {"created_at": time_str} + if not PYDANTIC_V2: with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): m.to_dict(warnings=False) @@ -558,9 +554,6 @@ class Model(BaseModel): assert m3.model_dump(exclude_none=True) == {} if not PYDANTIC_V2: - with pytest.raises(ValueError, match="mode is only supported in Pydantic v2"): - m.model_dump(mode="json") - with pytest.raises(ValueError, match="round_trip is only supported in Pydantic v2"): m.model_dump(round_trip=True) diff --git a/tests/test_transform.py b/tests/test_transform.py index 9e18f6fe..2c1448c5 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -177,17 +177,32 @@ class DateDict(TypedDict, total=False): foo: Annotated[date, PropertyInfo(format="iso8601")] +class DatetimeModel(BaseModel): + foo: datetime + + +class DateModel(BaseModel): + foo: Optional[date] + + @parametrize @pytest.mark.asyncio async def test_iso8601_format(use_async: bool) -> None: dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") + tz = "Z" if PYDANTIC_V2 else "+00:00" assert await transform({"foo": dt}, DatetimeDict, use_async) == {"foo": "2023-02-23T14:16:36.337692+00:00"} # type: ignore[comparison-overlap] + assert await transform(DatetimeModel(foo=dt), Any, use_async) == {"foo": "2023-02-23T14:16:36.337692" + tz} # type: ignore[comparison-overlap] dt = dt.replace(tzinfo=None) assert await transform({"foo": dt}, DatetimeDict, use_async) == {"foo": "2023-02-23T14:16:36.337692"} # type: ignore[comparison-overlap] + assert await transform(DatetimeModel(foo=dt), Any, use_async) == {"foo": "2023-02-23T14:16:36.337692"} # type: ignore[comparison-overlap] assert await transform({"foo": None}, DateDict, use_async) == {"foo": None} # type: ignore[comparison-overlap] + assert await transform(DateModel(foo=None), Any, use_async) == {"foo": None} # type: ignore assert await transform({"foo": date.fromisoformat("2023-02-23")}, DateDict, use_async) == {"foo": "2023-02-23"} # type: ignore[comparison-overlap] + assert await transform(DateModel(foo=date.fromisoformat("2023-02-23")), DateDict, use_async) == { + "foo": "2023-02-23" + } # type: ignore[comparison-overlap] @parametrize From 36df0f8ec6314063eaf3a9d5cf4baecbc32b523b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 6 Nov 2024 19:26:58 +0000 Subject: [PATCH 109/399] chore: rebuild project due to codegen change (#106) --- README.md | 4 ++-- pyproject.toml | 5 ++--- tests/test_client.py | 4 ++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index bf865c51..16aa2f68 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![PyPI version](https://img.shields.io/pypi/v/writer-sdk.svg)](https://pypi.org/project/writer-sdk/) -The Writer Python library provides convenient access to the Writer REST API from any Python 3.7+ +The Writer Python library provides convenient access to the Writer REST API from any Python 3.8+ application. The library includes type definitions for all request params and response fields, and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx). @@ -434,7 +434,7 @@ print(writerai.__version__) ## Requirements -Python 3.7 or higher. +Python 3.8 or higher. ## Contributing diff --git a/pyproject.toml b/pyproject.toml index 0cb09bd9..046e4fb4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,11 +16,10 @@ dependencies = [ "sniffio", "cached-property; python_version < '3.8'", ] -requires-python = ">= 3.7" +requires-python = ">= 3.8" classifiers = [ "Typing :: Typed", "Intended Audience :: Developers", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -139,7 +138,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.7" +pythonVersion = "3.8" exclude = [ "_dev", diff --git a/tests/test_client.py b/tests/test_client.py index 92e90eec..8d6c67a5 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -703,7 +703,7 @@ class Model(BaseModel): [3, "", 0.5], [2, "", 0.5 * 2.0], [1, "", 0.5 * 4.0], - [-1100, "", 7.8], # test large number potentially overflowing + [-1100, "", 8], # test large number potentially overflowing ], ) @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) @@ -1494,7 +1494,7 @@ class Model(BaseModel): [3, "", 0.5], [2, "", 0.5 * 2.0], [1, "", 0.5 * 4.0], - [-1100, "", 7.8], # test large number potentially overflowing + [-1100, "", 8], # test large number potentially overflowing ], ) @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) From 487dd7c44e5aef73c7bb8385ba909c1108ffabb9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 7 Nov 2024 20:52:34 +0000 Subject: [PATCH 110/399] feat(api): update tools api methods (#107) --- api.md | 21 +-- src/writerai/resources/tools/__init__.py | 40 ++-- .../tools/{medical.py => comprehend.py} | 82 ++++---- src/writerai/resources/tools/pdf_parser.py | 178 ------------------ src/writerai/resources/tools/tools.py | 159 +++++++++++----- src/writerai/types/__init__.py | 2 + ...rse_params.py => tool_parse_pdf_params.py} | 4 +- ...response.py => tool_parse_pdf_response.py} | 6 +- src/writerai/types/tools/__init__.py | 6 +- ...params.py => comprehend_medical_params.py} | 4 +- ...onse.py => comprehend_medical_response.py} | 4 +- tests/api_resources/test_tools.py | 89 ++++++++- .../{test_medical.py => test_comprehend.py} | 50 ++--- tests/api_resources/tools/test_pdf_parser.py | 106 ----------- 14 files changed, 292 insertions(+), 459 deletions(-) rename src/writerai/resources/tools/{medical.py => comprehend.py} (70%) delete mode 100644 src/writerai/resources/tools/pdf_parser.py rename src/writerai/types/{tools/pdf_parser_parse_params.py => tool_parse_pdf_params.py} (77%) rename src/writerai/types/{tools/pdf_parser_parse_response.py => tool_parse_pdf_response.py} (63%) rename src/writerai/types/tools/{medical_create_params.py => comprehend_medical_params.py} (85%) rename src/writerai/types/tools/{medical_create_response.py => comprehend_medical_response.py} (94%) rename tests/api_resources/tools/{test_medical.py => test_comprehend.py} (51%) delete mode 100644 tests/api_resources/tools/test_pdf_parser.py diff --git a/api.md b/api.md index db3ab80f..eb822510 100644 --- a/api.md +++ b/api.md @@ -94,33 +94,22 @@ Methods: Types: ```python -from writerai.types import ToolContextAwareSplittingResponse +from writerai.types import ToolContextAwareSplittingResponse, ToolParsePdfResponse ``` Methods: - client.tools.context_aware_splitting(\*\*params) -> ToolContextAwareSplittingResponse +- client.tools.parse_pdf(file_id, \*\*params) -> ToolParsePdfResponse -## Medical +## Comprehend Types: ```python -from writerai.types.tools import MedicalCreateResponse +from writerai.types.tools import ComprehendMedicalResponse ``` Methods: -- client.tools.medical.create(\*\*params) -> MedicalCreateResponse - -## PdfParser - -Types: - -```python -from writerai.types.tools import PdfParserParseResponse -``` - -Methods: - -- client.tools.pdf_parser.parse(file_id, \*\*params) -> PdfParserParseResponse +- client.tools.comprehend.medical(\*\*params) -> ComprehendMedicalResponse diff --git a/src/writerai/resources/tools/__init__.py b/src/writerai/resources/tools/__init__.py index 277efbb8..8f4ceef3 100644 --- a/src/writerai/resources/tools/__init__.py +++ b/src/writerai/resources/tools/__init__.py @@ -8,36 +8,22 @@ ToolsResourceWithStreamingResponse, AsyncToolsResourceWithStreamingResponse, ) -from .medical import ( - MedicalResource, - AsyncMedicalResource, - MedicalResourceWithRawResponse, - AsyncMedicalResourceWithRawResponse, - MedicalResourceWithStreamingResponse, - AsyncMedicalResourceWithStreamingResponse, -) -from .pdf_parser import ( - PdfParserResource, - AsyncPdfParserResource, - PdfParserResourceWithRawResponse, - AsyncPdfParserResourceWithRawResponse, - PdfParserResourceWithStreamingResponse, - AsyncPdfParserResourceWithStreamingResponse, +from .comprehend import ( + ComprehendResource, + AsyncComprehendResource, + ComprehendResourceWithRawResponse, + AsyncComprehendResourceWithRawResponse, + ComprehendResourceWithStreamingResponse, + AsyncComprehendResourceWithStreamingResponse, ) __all__ = [ - "MedicalResource", - "AsyncMedicalResource", - "MedicalResourceWithRawResponse", - "AsyncMedicalResourceWithRawResponse", - "MedicalResourceWithStreamingResponse", - "AsyncMedicalResourceWithStreamingResponse", - "PdfParserResource", - "AsyncPdfParserResource", - "PdfParserResourceWithRawResponse", - "AsyncPdfParserResourceWithRawResponse", - "PdfParserResourceWithStreamingResponse", - "AsyncPdfParserResourceWithStreamingResponse", + "ComprehendResource", + "AsyncComprehendResource", + "ComprehendResourceWithRawResponse", + "AsyncComprehendResourceWithRawResponse", + "ComprehendResourceWithStreamingResponse", + "AsyncComprehendResourceWithStreamingResponse", "ToolsResource", "AsyncToolsResource", "ToolsResourceWithRawResponse", diff --git a/src/writerai/resources/tools/medical.py b/src/writerai/resources/tools/comprehend.py similarity index 70% rename from src/writerai/resources/tools/medical.py rename to src/writerai/resources/tools/comprehend.py index e3b10e22..a22caa80 100644 --- a/src/writerai/resources/tools/medical.py +++ b/src/writerai/resources/tools/comprehend.py @@ -19,34 +19,34 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from ...types.tools import medical_create_params +from ...types.tools import comprehend_medical_params from ..._base_client import make_request_options -from ...types.tools.medical_create_response import MedicalCreateResponse +from ...types.tools.comprehend_medical_response import ComprehendMedicalResponse -__all__ = ["MedicalResource", "AsyncMedicalResource"] +__all__ = ["ComprehendResource", "AsyncComprehendResource"] -class MedicalResource(SyncAPIResource): +class ComprehendResource(SyncAPIResource): @cached_property - def with_raw_response(self) -> MedicalResourceWithRawResponse: + def with_raw_response(self) -> ComprehendResourceWithRawResponse: """ This property can be used as a prefix for any HTTP method call to return the the raw response object instead of the parsed content. For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers """ - return MedicalResourceWithRawResponse(self) + return ComprehendResourceWithRawResponse(self) @cached_property - def with_streaming_response(self) -> MedicalResourceWithStreamingResponse: + def with_streaming_response(self) -> ComprehendResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. For more information, see https://www.github.com/writer/writer-python#with_streaming_response """ - return MedicalResourceWithStreamingResponse(self) + return ComprehendResourceWithStreamingResponse(self) - def create( + def medical( self, *, content: str, @@ -57,7 +57,7 @@ def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> MedicalCreateResponse: + ) -> ComprehendMedicalResponse: """ Create a completion using Palmyra medical model. @@ -83,36 +83,36 @@ def create( "content": content, "response_type": response_type, }, - medical_create_params.MedicalCreateParams, + comprehend_medical_params.ComprehendMedicalParams, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=MedicalCreateResponse, + cast_to=ComprehendMedicalResponse, ) -class AsyncMedicalResource(AsyncAPIResource): +class AsyncComprehendResource(AsyncAPIResource): @cached_property - def with_raw_response(self) -> AsyncMedicalResourceWithRawResponse: + def with_raw_response(self) -> AsyncComprehendResourceWithRawResponse: """ This property can be used as a prefix for any HTTP method call to return the the raw response object instead of the parsed content. For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers """ - return AsyncMedicalResourceWithRawResponse(self) + return AsyncComprehendResourceWithRawResponse(self) @cached_property - def with_streaming_response(self) -> AsyncMedicalResourceWithStreamingResponse: + def with_streaming_response(self) -> AsyncComprehendResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. For more information, see https://www.github.com/writer/writer-python#with_streaming_response """ - return AsyncMedicalResourceWithStreamingResponse(self) + return AsyncComprehendResourceWithStreamingResponse(self) - async def create( + async def medical( self, *, content: str, @@ -123,7 +123,7 @@ async def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> MedicalCreateResponse: + ) -> ComprehendMedicalResponse: """ Create a completion using Palmyra medical model. @@ -149,46 +149,46 @@ async def create( "content": content, "response_type": response_type, }, - medical_create_params.MedicalCreateParams, + comprehend_medical_params.ComprehendMedicalParams, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=MedicalCreateResponse, + cast_to=ComprehendMedicalResponse, ) -class MedicalResourceWithRawResponse: - def __init__(self, medical: MedicalResource) -> None: - self._medical = medical +class ComprehendResourceWithRawResponse: + def __init__(self, comprehend: ComprehendResource) -> None: + self._comprehend = comprehend - self.create = to_raw_response_wrapper( - medical.create, + self.medical = to_raw_response_wrapper( + comprehend.medical, ) -class AsyncMedicalResourceWithRawResponse: - def __init__(self, medical: AsyncMedicalResource) -> None: - self._medical = medical +class AsyncComprehendResourceWithRawResponse: + def __init__(self, comprehend: AsyncComprehendResource) -> None: + self._comprehend = comprehend - self.create = async_to_raw_response_wrapper( - medical.create, + self.medical = async_to_raw_response_wrapper( + comprehend.medical, ) -class MedicalResourceWithStreamingResponse: - def __init__(self, medical: MedicalResource) -> None: - self._medical = medical +class ComprehendResourceWithStreamingResponse: + def __init__(self, comprehend: ComprehendResource) -> None: + self._comprehend = comprehend - self.create = to_streamed_response_wrapper( - medical.create, + self.medical = to_streamed_response_wrapper( + comprehend.medical, ) -class AsyncMedicalResourceWithStreamingResponse: - def __init__(self, medical: AsyncMedicalResource) -> None: - self._medical = medical +class AsyncComprehendResourceWithStreamingResponse: + def __init__(self, comprehend: AsyncComprehendResource) -> None: + self._comprehend = comprehend - self.create = async_to_streamed_response_wrapper( - medical.create, + self.medical = async_to_streamed_response_wrapper( + comprehend.medical, ) diff --git a/src/writerai/resources/tools/pdf_parser.py b/src/writerai/resources/tools/pdf_parser.py deleted file mode 100644 index 6b9cd379..00000000 --- a/src/writerai/resources/tools/pdf_parser.py +++ /dev/null @@ -1,178 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Literal - -import httpx - -from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from ..._utils import ( - maybe_transform, - async_maybe_transform, -) -from ..._compat import cached_property -from ..._resource import SyncAPIResource, AsyncAPIResource -from ..._response import ( - to_raw_response_wrapper, - to_streamed_response_wrapper, - async_to_raw_response_wrapper, - async_to_streamed_response_wrapper, -) -from ...types.tools import pdf_parser_parse_params -from ..._base_client import make_request_options -from ...types.tools.pdf_parser_parse_response import PdfParserParseResponse - -__all__ = ["PdfParserResource", "AsyncPdfParserResource"] - - -class PdfParserResource(SyncAPIResource): - @cached_property - def with_raw_response(self) -> PdfParserResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return the - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers - """ - return PdfParserResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> PdfParserResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/writer/writer-python#with_streaming_response - """ - return PdfParserResourceWithStreamingResponse(self) - - def parse( - self, - file_id: str, - *, - format: Literal["text", "markdown"], - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> PdfParserParseResponse: - """ - Parse PDF to other formats. - - Args: - format: The format into which the PDF content should be converted. - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not file_id: - raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") - return self._post( - f"/v1/tools/pdf-parser/{file_id}", - body=maybe_transform({"format": format}, pdf_parser_parse_params.PdfParserParseParams), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=PdfParserParseResponse, - ) - - -class AsyncPdfParserResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncPdfParserResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return the - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers - """ - return AsyncPdfParserResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncPdfParserResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/writer/writer-python#with_streaming_response - """ - return AsyncPdfParserResourceWithStreamingResponse(self) - - async def parse( - self, - file_id: str, - *, - format: Literal["text", "markdown"], - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> PdfParserParseResponse: - """ - Parse PDF to other formats. - - Args: - format: The format into which the PDF content should be converted. - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not file_id: - raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") - return await self._post( - f"/v1/tools/pdf-parser/{file_id}", - body=await async_maybe_transform({"format": format}, pdf_parser_parse_params.PdfParserParseParams), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=PdfParserParseResponse, - ) - - -class PdfParserResourceWithRawResponse: - def __init__(self, pdf_parser: PdfParserResource) -> None: - self._pdf_parser = pdf_parser - - self.parse = to_raw_response_wrapper( - pdf_parser.parse, - ) - - -class AsyncPdfParserResourceWithRawResponse: - def __init__(self, pdf_parser: AsyncPdfParserResource) -> None: - self._pdf_parser = pdf_parser - - self.parse = async_to_raw_response_wrapper( - pdf_parser.parse, - ) - - -class PdfParserResourceWithStreamingResponse: - def __init__(self, pdf_parser: PdfParserResource) -> None: - self._pdf_parser = pdf_parser - - self.parse = to_streamed_response_wrapper( - pdf_parser.parse, - ) - - -class AsyncPdfParserResourceWithStreamingResponse: - def __init__(self, pdf_parser: AsyncPdfParserResource) -> None: - self._pdf_parser = pdf_parser - - self.parse = async_to_streamed_response_wrapper( - pdf_parser.parse, - ) diff --git a/src/writerai/resources/tools/tools.py b/src/writerai/resources/tools/tools.py index d8e3d934..8809cf20 100644 --- a/src/writerai/resources/tools/tools.py +++ b/src/writerai/resources/tools/tools.py @@ -6,28 +6,20 @@ import httpx -from ...types import tool_context_aware_splitting_params -from .medical import ( - MedicalResource, - AsyncMedicalResource, - MedicalResourceWithRawResponse, - AsyncMedicalResourceWithRawResponse, - MedicalResourceWithStreamingResponse, - AsyncMedicalResourceWithStreamingResponse, -) +from ...types import tool_parse_pdf_params, tool_context_aware_splitting_params from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven from ..._utils import ( maybe_transform, async_maybe_transform, ) from ..._compat import cached_property -from .pdf_parser import ( - PdfParserResource, - AsyncPdfParserResource, - PdfParserResourceWithRawResponse, - AsyncPdfParserResourceWithRawResponse, - PdfParserResourceWithStreamingResponse, - AsyncPdfParserResourceWithStreamingResponse, +from .comprehend import ( + ComprehendResource, + AsyncComprehendResource, + ComprehendResourceWithRawResponse, + AsyncComprehendResourceWithRawResponse, + ComprehendResourceWithStreamingResponse, + AsyncComprehendResourceWithStreamingResponse, ) from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -37,6 +29,7 @@ async_to_streamed_response_wrapper, ) from ..._base_client import make_request_options +from ...types.tool_parse_pdf_response import ToolParsePdfResponse from ...types.tool_context_aware_splitting_response import ToolContextAwareSplittingResponse __all__ = ["ToolsResource", "AsyncToolsResource"] @@ -44,12 +37,8 @@ class ToolsResource(SyncAPIResource): @cached_property - def medical(self) -> MedicalResource: - return MedicalResource(self._client) - - @cached_property - def pdf_parser(self) -> PdfParserResource: - return PdfParserResource(self._client) + def comprehend(self) -> ComprehendResource: + return ComprehendResource(self._client) @cached_property def with_raw_response(self) -> ToolsResourceWithRawResponse: @@ -116,15 +105,48 @@ def context_aware_splitting( cast_to=ToolContextAwareSplittingResponse, ) + def parse_pdf( + self, + file_id: str, + *, + format: Literal["text", "markdown"], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ToolParsePdfResponse: + """ + Parse PDF to other formats. + + Args: + format: The format into which the PDF content should be converted. -class AsyncToolsResource(AsyncAPIResource): - @cached_property - def medical(self) -> AsyncMedicalResource: - return AsyncMedicalResource(self._client) + extra_headers: Send extra headers + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + return self._post( + f"/v1/tools/pdf-parser/{file_id}", + body=maybe_transform({"format": format}, tool_parse_pdf_params.ToolParsePdfParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ToolParsePdfResponse, + ) + + +class AsyncToolsResource(AsyncAPIResource): @cached_property - def pdf_parser(self) -> AsyncPdfParserResource: - return AsyncPdfParserResource(self._client) + def comprehend(self) -> AsyncComprehendResource: + return AsyncComprehendResource(self._client) @cached_property def with_raw_response(self) -> AsyncToolsResourceWithRawResponse: @@ -191,6 +213,43 @@ async def context_aware_splitting( cast_to=ToolContextAwareSplittingResponse, ) + async def parse_pdf( + self, + file_id: str, + *, + format: Literal["text", "markdown"], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ToolParsePdfResponse: + """ + Parse PDF to other formats. + + Args: + format: The format into which the PDF content should be converted. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + return await self._post( + f"/v1/tools/pdf-parser/{file_id}", + body=await async_maybe_transform({"format": format}, tool_parse_pdf_params.ToolParsePdfParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ToolParsePdfResponse, + ) + class ToolsResourceWithRawResponse: def __init__(self, tools: ToolsResource) -> None: @@ -199,14 +258,13 @@ def __init__(self, tools: ToolsResource) -> None: self.context_aware_splitting = to_raw_response_wrapper( tools.context_aware_splitting, ) + self.parse_pdf = to_raw_response_wrapper( + tools.parse_pdf, + ) @cached_property - def medical(self) -> MedicalResourceWithRawResponse: - return MedicalResourceWithRawResponse(self._tools.medical) - - @cached_property - def pdf_parser(self) -> PdfParserResourceWithRawResponse: - return PdfParserResourceWithRawResponse(self._tools.pdf_parser) + def comprehend(self) -> ComprehendResourceWithRawResponse: + return ComprehendResourceWithRawResponse(self._tools.comprehend) class AsyncToolsResourceWithRawResponse: @@ -216,14 +274,13 @@ def __init__(self, tools: AsyncToolsResource) -> None: self.context_aware_splitting = async_to_raw_response_wrapper( tools.context_aware_splitting, ) + self.parse_pdf = async_to_raw_response_wrapper( + tools.parse_pdf, + ) @cached_property - def medical(self) -> AsyncMedicalResourceWithRawResponse: - return AsyncMedicalResourceWithRawResponse(self._tools.medical) - - @cached_property - def pdf_parser(self) -> AsyncPdfParserResourceWithRawResponse: - return AsyncPdfParserResourceWithRawResponse(self._tools.pdf_parser) + def comprehend(self) -> AsyncComprehendResourceWithRawResponse: + return AsyncComprehendResourceWithRawResponse(self._tools.comprehend) class ToolsResourceWithStreamingResponse: @@ -233,14 +290,13 @@ def __init__(self, tools: ToolsResource) -> None: self.context_aware_splitting = to_streamed_response_wrapper( tools.context_aware_splitting, ) + self.parse_pdf = to_streamed_response_wrapper( + tools.parse_pdf, + ) @cached_property - def medical(self) -> MedicalResourceWithStreamingResponse: - return MedicalResourceWithStreamingResponse(self._tools.medical) - - @cached_property - def pdf_parser(self) -> PdfParserResourceWithStreamingResponse: - return PdfParserResourceWithStreamingResponse(self._tools.pdf_parser) + def comprehend(self) -> ComprehendResourceWithStreamingResponse: + return ComprehendResourceWithStreamingResponse(self._tools.comprehend) class AsyncToolsResourceWithStreamingResponse: @@ -250,11 +306,10 @@ def __init__(self, tools: AsyncToolsResource) -> None: self.context_aware_splitting = async_to_streamed_response_wrapper( tools.context_aware_splitting, ) + self.parse_pdf = async_to_streamed_response_wrapper( + tools.parse_pdf, + ) @cached_property - def medical(self) -> AsyncMedicalResourceWithStreamingResponse: - return AsyncMedicalResourceWithStreamingResponse(self._tools.medical) - - @cached_property - def pdf_parser(self) -> AsyncPdfParserResourceWithStreamingResponse: - return AsyncPdfParserResourceWithStreamingResponse(self._tools.pdf_parser) + def comprehend(self) -> AsyncComprehendResourceWithStreamingResponse: + return AsyncComprehendResourceWithStreamingResponse(self._tools.comprehend) diff --git a/src/writerai/types/__init__.py b/src/writerai/types/__init__.py index 6646f0fb..d1a9a1e6 100644 --- a/src/writerai/types/__init__.py +++ b/src/writerai/types/__init__.py @@ -23,6 +23,8 @@ from .graph_delete_response import GraphDeleteResponse as GraphDeleteResponse from .graph_question_params import GraphQuestionParams as GraphQuestionParams from .graph_update_response import GraphUpdateResponse as GraphUpdateResponse +from .tool_parse_pdf_params import ToolParsePdfParams as ToolParsePdfParams +from .tool_parse_pdf_response import ToolParsePdfResponse as ToolParsePdfResponse from .completion_create_params import CompletionCreateParams as CompletionCreateParams from .graph_add_file_to_graph_params import GraphAddFileToGraphParams as GraphAddFileToGraphParams from .application_generate_content_params import ApplicationGenerateContentParams as ApplicationGenerateContentParams diff --git a/src/writerai/types/tools/pdf_parser_parse_params.py b/src/writerai/types/tool_parse_pdf_params.py similarity index 77% rename from src/writerai/types/tools/pdf_parser_parse_params.py rename to src/writerai/types/tool_parse_pdf_params.py index 5cbb30e2..52c535c2 100644 --- a/src/writerai/types/tools/pdf_parser_parse_params.py +++ b/src/writerai/types/tool_parse_pdf_params.py @@ -4,9 +4,9 @@ from typing_extensions import Literal, Required, TypedDict -__all__ = ["PdfParserParseParams"] +__all__ = ["ToolParsePdfParams"] -class PdfParserParseParams(TypedDict, total=False): +class ToolParsePdfParams(TypedDict, total=False): format: Required[Literal["text", "markdown"]] """The format into which the PDF content should be converted.""" diff --git a/src/writerai/types/tools/pdf_parser_parse_response.py b/src/writerai/types/tool_parse_pdf_response.py similarity index 63% rename from src/writerai/types/tools/pdf_parser_parse_response.py rename to src/writerai/types/tool_parse_pdf_response.py index 15390b01..d7ccf851 100644 --- a/src/writerai/types/tools/pdf_parser_parse_response.py +++ b/src/writerai/types/tool_parse_pdf_response.py @@ -1,11 +1,11 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from ..._models import BaseModel +from .._models import BaseModel -__all__ = ["PdfParserParseResponse"] +__all__ = ["ToolParsePdfResponse"] -class PdfParserParseResponse(BaseModel): +class ToolParsePdfResponse(BaseModel): content: str """The extracted content from the PDF file, converted to the specified format.""" diff --git a/src/writerai/types/tools/__init__.py b/src/writerai/types/tools/__init__.py index fd852e5e..23e03174 100644 --- a/src/writerai/types/tools/__init__.py +++ b/src/writerai/types/tools/__init__.py @@ -2,7 +2,5 @@ from __future__ import annotations -from .medical_create_params import MedicalCreateParams as MedicalCreateParams -from .medical_create_response import MedicalCreateResponse as MedicalCreateResponse -from .pdf_parser_parse_params import PdfParserParseParams as PdfParserParseParams -from .pdf_parser_parse_response import PdfParserParseResponse as PdfParserParseResponse +from .comprehend_medical_params import ComprehendMedicalParams as ComprehendMedicalParams +from .comprehend_medical_response import ComprehendMedicalResponse as ComprehendMedicalResponse diff --git a/src/writerai/types/tools/medical_create_params.py b/src/writerai/types/tools/comprehend_medical_params.py similarity index 85% rename from src/writerai/types/tools/medical_create_params.py rename to src/writerai/types/tools/comprehend_medical_params.py index 3c15f70a..8418dc6a 100644 --- a/src/writerai/types/tools/medical_create_params.py +++ b/src/writerai/types/tools/comprehend_medical_params.py @@ -4,10 +4,10 @@ from typing_extensions import Literal, Required, TypedDict -__all__ = ["MedicalCreateParams"] +__all__ = ["ComprehendMedicalParams"] -class MedicalCreateParams(TypedDict, total=False): +class ComprehendMedicalParams(TypedDict, total=False): content: Required[str] """The text to be analyzed.""" diff --git a/src/writerai/types/tools/medical_create_response.py b/src/writerai/types/tools/comprehend_medical_response.py similarity index 94% rename from src/writerai/types/tools/medical_create_response.py rename to src/writerai/types/tools/comprehend_medical_response.py index 28ebd094..9489f389 100644 --- a/src/writerai/types/tools/medical_create_response.py +++ b/src/writerai/types/tools/comprehend_medical_response.py @@ -5,7 +5,7 @@ from ..._models import BaseModel __all__ = [ - "MedicalCreateResponse", + "ComprehendMedicalResponse", "Entity", "EntityAttribute", "EntityAttributeConcept", @@ -85,6 +85,6 @@ class Entity(BaseModel): type: str -class MedicalCreateResponse(BaseModel): +class ComprehendMedicalResponse(BaseModel): entities: List[Entity] """An array of medical entities extracted from the input text.""" diff --git a/tests/api_resources/test_tools.py b/tests/api_resources/test_tools.py index b5d8b828..0fa0fdc0 100644 --- a/tests/api_resources/test_tools.py +++ b/tests/api_resources/test_tools.py @@ -9,7 +9,10 @@ from writerai import Writer, AsyncWriter from tests.utils import assert_matches_type -from writerai.types import ToolContextAwareSplittingResponse +from writerai.types import ( + ToolParsePdfResponse, + ToolContextAwareSplittingResponse, +) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -51,6 +54,48 @@ def test_streaming_response_context_aware_splitting(self, client: Writer) -> Non assert cast(Any, response.is_closed) is True + @parametrize + def test_method_parse_pdf(self, client: Writer) -> None: + tool = client.tools.parse_pdf( + file_id="file_id", + format="text", + ) + assert_matches_type(ToolParsePdfResponse, tool, path=["response"]) + + @parametrize + def test_raw_response_parse_pdf(self, client: Writer) -> None: + response = client.tools.with_raw_response.parse_pdf( + file_id="file_id", + format="text", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + tool = response.parse() + assert_matches_type(ToolParsePdfResponse, tool, path=["response"]) + + @parametrize + def test_streaming_response_parse_pdf(self, client: Writer) -> None: + with client.tools.with_streaming_response.parse_pdf( + file_id="file_id", + format="text", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + tool = response.parse() + assert_matches_type(ToolParsePdfResponse, tool, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_parse_pdf(self, client: Writer) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): + client.tools.with_raw_response.parse_pdf( + file_id="", + format="text", + ) + class TestAsyncTools: parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) @@ -88,3 +133,45 @@ async def test_streaming_response_context_aware_splitting(self, async_client: As assert_matches_type(ToolContextAwareSplittingResponse, tool, path=["response"]) assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_parse_pdf(self, async_client: AsyncWriter) -> None: + tool = await async_client.tools.parse_pdf( + file_id="file_id", + format="text", + ) + assert_matches_type(ToolParsePdfResponse, tool, path=["response"]) + + @parametrize + async def test_raw_response_parse_pdf(self, async_client: AsyncWriter) -> None: + response = await async_client.tools.with_raw_response.parse_pdf( + file_id="file_id", + format="text", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + tool = await response.parse() + assert_matches_type(ToolParsePdfResponse, tool, path=["response"]) + + @parametrize + async def test_streaming_response_parse_pdf(self, async_client: AsyncWriter) -> None: + async with async_client.tools.with_streaming_response.parse_pdf( + file_id="file_id", + format="text", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + tool = await response.parse() + assert_matches_type(ToolParsePdfResponse, tool, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_parse_pdf(self, async_client: AsyncWriter) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): + await async_client.tools.with_raw_response.parse_pdf( + file_id="", + format="text", + ) diff --git a/tests/api_resources/tools/test_medical.py b/tests/api_resources/tools/test_comprehend.py similarity index 51% rename from tests/api_resources/tools/test_medical.py rename to tests/api_resources/tools/test_comprehend.py index c2592b2e..bf026b75 100644 --- a/tests/api_resources/tools/test_medical.py +++ b/tests/api_resources/tools/test_comprehend.py @@ -9,82 +9,82 @@ from writerai import Writer, AsyncWriter from tests.utils import assert_matches_type -from writerai.types.tools import MedicalCreateResponse +from writerai.types.tools import ComprehendMedicalResponse base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") -class TestMedical: +class TestComprehend: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) @parametrize - def test_method_create(self, client: Writer) -> None: - medical = client.tools.medical.create( + def test_method_medical(self, client: Writer) -> None: + comprehend = client.tools.comprehend.medical( content="content", response_type="Entities", ) - assert_matches_type(MedicalCreateResponse, medical, path=["response"]) + assert_matches_type(ComprehendMedicalResponse, comprehend, path=["response"]) @parametrize - def test_raw_response_create(self, client: Writer) -> None: - response = client.tools.medical.with_raw_response.create( + def test_raw_response_medical(self, client: Writer) -> None: + response = client.tools.comprehend.with_raw_response.medical( content="content", response_type="Entities", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" - medical = response.parse() - assert_matches_type(MedicalCreateResponse, medical, path=["response"]) + comprehend = response.parse() + assert_matches_type(ComprehendMedicalResponse, comprehend, path=["response"]) @parametrize - def test_streaming_response_create(self, client: Writer) -> None: - with client.tools.medical.with_streaming_response.create( + def test_streaming_response_medical(self, client: Writer) -> None: + with client.tools.comprehend.with_streaming_response.medical( content="content", response_type="Entities", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" - medical = response.parse() - assert_matches_type(MedicalCreateResponse, medical, path=["response"]) + comprehend = response.parse() + assert_matches_type(ComprehendMedicalResponse, comprehend, path=["response"]) assert cast(Any, response.is_closed) is True -class TestAsyncMedical: +class TestAsyncComprehend: parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) @parametrize - async def test_method_create(self, async_client: AsyncWriter) -> None: - medical = await async_client.tools.medical.create( + async def test_method_medical(self, async_client: AsyncWriter) -> None: + comprehend = await async_client.tools.comprehend.medical( content="content", response_type="Entities", ) - assert_matches_type(MedicalCreateResponse, medical, path=["response"]) + assert_matches_type(ComprehendMedicalResponse, comprehend, path=["response"]) @parametrize - async def test_raw_response_create(self, async_client: AsyncWriter) -> None: - response = await async_client.tools.medical.with_raw_response.create( + async def test_raw_response_medical(self, async_client: AsyncWriter) -> None: + response = await async_client.tools.comprehend.with_raw_response.medical( content="content", response_type="Entities", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" - medical = await response.parse() - assert_matches_type(MedicalCreateResponse, medical, path=["response"]) + comprehend = await response.parse() + assert_matches_type(ComprehendMedicalResponse, comprehend, path=["response"]) @parametrize - async def test_streaming_response_create(self, async_client: AsyncWriter) -> None: - async with async_client.tools.medical.with_streaming_response.create( + async def test_streaming_response_medical(self, async_client: AsyncWriter) -> None: + async with async_client.tools.comprehend.with_streaming_response.medical( content="content", response_type="Entities", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" - medical = await response.parse() - assert_matches_type(MedicalCreateResponse, medical, path=["response"]) + comprehend = await response.parse() + assert_matches_type(ComprehendMedicalResponse, comprehend, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/tools/test_pdf_parser.py b/tests/api_resources/tools/test_pdf_parser.py deleted file mode 100644 index 5d711a3e..00000000 --- a/tests/api_resources/tools/test_pdf_parser.py +++ /dev/null @@ -1,106 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import os -from typing import Any, cast - -import pytest - -from writerai import Writer, AsyncWriter -from tests.utils import assert_matches_type -from writerai.types.tools import PdfParserParseResponse - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestPdfParser: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @parametrize - def test_method_parse(self, client: Writer) -> None: - pdf_parser = client.tools.pdf_parser.parse( - file_id="file_id", - format="text", - ) - assert_matches_type(PdfParserParseResponse, pdf_parser, path=["response"]) - - @parametrize - def test_raw_response_parse(self, client: Writer) -> None: - response = client.tools.pdf_parser.with_raw_response.parse( - file_id="file_id", - format="text", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - pdf_parser = response.parse() - assert_matches_type(PdfParserParseResponse, pdf_parser, path=["response"]) - - @parametrize - def test_streaming_response_parse(self, client: Writer) -> None: - with client.tools.pdf_parser.with_streaming_response.parse( - file_id="file_id", - format="text", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - pdf_parser = response.parse() - assert_matches_type(PdfParserParseResponse, pdf_parser, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @parametrize - def test_path_params_parse(self, client: Writer) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): - client.tools.pdf_parser.with_raw_response.parse( - file_id="", - format="text", - ) - - -class TestAsyncPdfParser: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) - - @parametrize - async def test_method_parse(self, async_client: AsyncWriter) -> None: - pdf_parser = await async_client.tools.pdf_parser.parse( - file_id="file_id", - format="text", - ) - assert_matches_type(PdfParserParseResponse, pdf_parser, path=["response"]) - - @parametrize - async def test_raw_response_parse(self, async_client: AsyncWriter) -> None: - response = await async_client.tools.pdf_parser.with_raw_response.parse( - file_id="file_id", - format="text", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - pdf_parser = await response.parse() - assert_matches_type(PdfParserParseResponse, pdf_parser, path=["response"]) - - @parametrize - async def test_streaming_response_parse(self, async_client: AsyncWriter) -> None: - async with async_client.tools.pdf_parser.with_streaming_response.parse( - file_id="file_id", - format="text", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - pdf_parser = await response.parse() - assert_matches_type(PdfParserParseResponse, pdf_parser, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @parametrize - async def test_path_params_parse(self, async_client: AsyncWriter) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): - await async_client.tools.pdf_parser.with_raw_response.parse( - file_id="", - format="text", - ) From a0067bfc190a341ae91de1d46700fcdf20f323b1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 8 Nov 2024 00:04:35 +0000 Subject: [PATCH 111/399] feat(api): api update (#108) --- .stats.yml | 2 +- src/writerai/resources/tools/comprehend.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index 1c1c3c05..a10b14ec 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-d13c05cc3a960ab336c11a4e7a4f31f10cdf4b9aee968c8f8534145e39fb374a.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-9c49d7cc176d84ba91accd64aa4a8c91a44cdce83e9ea59da8b259fa19ccf9f6.yml diff --git a/src/writerai/resources/tools/comprehend.py b/src/writerai/resources/tools/comprehend.py index a22caa80..b6432171 100644 --- a/src/writerai/resources/tools/comprehend.py +++ b/src/writerai/resources/tools/comprehend.py @@ -59,7 +59,8 @@ def medical( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ComprehendMedicalResponse: """ - Create a completion using Palmyra medical model. + Analyze unstructured medical text to extract entities labeled with standardized + medical codes and confidence scores. Args: content: The text to be analyzed. @@ -125,7 +126,8 @@ async def medical( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ComprehendMedicalResponse: """ - Create a completion using Palmyra medical model. + Analyze unstructured medical text to extract entities labeled with standardized + medical codes and confidence scores. Args: content: The text to be analyzed. From feabd787adee5c03f933b3bbd085c3a70f37ff9f Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Fri, 8 Nov 2024 17:35:51 +0000 Subject: [PATCH 112/399] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index a10b14ec..9eee2711 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-9c49d7cc176d84ba91accd64aa4a8c91a44cdce83e9ea59da8b259fa19ccf9f6.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-c350270f059b9d4c51d9bca0c34c3e3018e544f5daaf2d18aacc55a0561573f8.yml From 151c518102e6cf1fe152280bb081ea6c23a21d64 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 8 Nov 2024 17:41:55 +0000 Subject: [PATCH 113/399] feat(api): add streaming to kg question (#109) --- .stats.yml | 2 +- src/writerai/resources/graphs.py | 204 +++++++++++++++++++- src/writerai/types/graph_question_params.py | 28 ++- tests/api_resources/test_graphs.py | 102 ++++++++-- 4 files changed, 313 insertions(+), 23 deletions(-) diff --git a/.stats.yml b/.stats.yml index 9eee2711..a10b14ec 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-c350270f059b9d4c51d9bca0c34c3e3018e544f5daaf2d18aacc55a0561573f8.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-9c49d7cc176d84ba91accd64aa4a8c91a44cdce83e9ea59da8b259fa19ccf9f6.yml diff --git a/src/writerai/resources/graphs.py b/src/writerai/resources/graphs.py index 0ef90c74..9d9df1bc 100644 --- a/src/writerai/resources/graphs.py +++ b/src/writerai/resources/graphs.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import List -from typing_extensions import Literal +from typing_extensions import Literal, overload import httpx @@ -16,6 +16,7 @@ ) from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven from .._utils import ( + required_args, maybe_transform, async_maybe_transform, ) @@ -27,6 +28,7 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) +from .._streaming import Stream, AsyncStream from ..pagination import SyncCursorPage, AsyncCursorPage from ..types.file import File from ..types.graph import Graph @@ -314,12 +316,13 @@ def add_file_to_graph( cast_to=File, ) + @overload def question( self, *, graph_ids: List[str], question: str, - stream: bool, + stream: Literal[False], subqueries: bool, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -350,6 +353,101 @@ def question( timeout: Override the client-level default timeout for this request, in seconds """ + ... + + @overload + def question( + self, + *, + graph_ids: List[str], + question: str, + stream: Literal[True], + subqueries: bool, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Stream[Question]: + """ + Ask a question to specified Knowledge Graphs. + + Args: + graph_ids: The unique identifiers of the Knowledge Graphs to be queried. + + question: The question to be answered using the Knowledge Graph. + + stream: Determines whether the model's output should be streamed. If true, the output is + generated and sent incrementally, which can be useful for real-time + applications. + + subqueries: Specify whether to include subqueries. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + def question( + self, + *, + graph_ids: List[str], + question: str, + stream: bool, + subqueries: bool, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Question | Stream[Question]: + """ + Ask a question to specified Knowledge Graphs. + + Args: + graph_ids: The unique identifiers of the Knowledge Graphs to be queried. + + question: The question to be answered using the Knowledge Graph. + + stream: Determines whether the model's output should be streamed. If true, the output is + generated and sent incrementally, which can be useful for real-time + applications. + + subqueries: Specify whether to include subqueries. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @required_args(["graph_ids", "question", "stream", "subqueries"]) + def question( + self, + *, + graph_ids: List[str], + question: str, + stream: Literal[False] | Literal[True], + subqueries: bool, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Question | Stream[Question]: return self._post( "/v1/graphs/question", body=maybe_transform( @@ -365,6 +463,8 @@ def question( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=Question, + stream=stream or False, + stream_cls=Stream[Question], ) def remove_file_from_graph( @@ -680,12 +780,13 @@ async def add_file_to_graph( cast_to=File, ) + @overload async def question( self, *, graph_ids: List[str], question: str, - stream: bool, + stream: Literal[False], subqueries: bool, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -716,6 +817,101 @@ async def question( timeout: Override the client-level default timeout for this request, in seconds """ + ... + + @overload + async def question( + self, + *, + graph_ids: List[str], + question: str, + stream: Literal[True], + subqueries: bool, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncStream[Question]: + """ + Ask a question to specified Knowledge Graphs. + + Args: + graph_ids: The unique identifiers of the Knowledge Graphs to be queried. + + question: The question to be answered using the Knowledge Graph. + + stream: Determines whether the model's output should be streamed. If true, the output is + generated and sent incrementally, which can be useful for real-time + applications. + + subqueries: Specify whether to include subqueries. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + async def question( + self, + *, + graph_ids: List[str], + question: str, + stream: bool, + subqueries: bool, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Question | AsyncStream[Question]: + """ + Ask a question to specified Knowledge Graphs. + + Args: + graph_ids: The unique identifiers of the Knowledge Graphs to be queried. + + question: The question to be answered using the Knowledge Graph. + + stream: Determines whether the model's output should be streamed. If true, the output is + generated and sent incrementally, which can be useful for real-time + applications. + + subqueries: Specify whether to include subqueries. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @required_args(["graph_ids", "question", "stream", "subqueries"]) + async def question( + self, + *, + graph_ids: List[str], + question: str, + stream: Literal[False] | Literal[True], + subqueries: bool, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Question | AsyncStream[Question]: return await self._post( "/v1/graphs/question", body=await async_maybe_transform( @@ -731,6 +927,8 @@ async def question( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=Question, + stream=stream or False, + stream_cls=AsyncStream[Question], ) async def remove_file_from_graph( diff --git a/src/writerai/types/graph_question_params.py b/src/writerai/types/graph_question_params.py index acbc4fc1..508d10ae 100644 --- a/src/writerai/types/graph_question_params.py +++ b/src/writerai/types/graph_question_params.py @@ -2,25 +2,39 @@ from __future__ import annotations -from typing import List -from typing_extensions import Required, TypedDict +from typing import List, Union +from typing_extensions import Literal, Required, TypedDict -__all__ = ["GraphQuestionParams"] +__all__ = ["GraphQuestionParamsBase", "GraphQuestionParamsNonStreaming", "GraphQuestionParamsStreaming"] -class GraphQuestionParams(TypedDict, total=False): +class GraphQuestionParamsBase(TypedDict, total=False): graph_ids: Required[List[str]] """The unique identifiers of the Knowledge Graphs to be queried.""" question: Required[str] """The question to be answered using the Knowledge Graph.""" - stream: Required[bool] + subqueries: Required[bool] + """Specify whether to include subqueries.""" + + +class GraphQuestionParamsNonStreaming(GraphQuestionParamsBase, total=False): + stream: Required[Literal[False]] """Determines whether the model's output should be streamed. If true, the output is generated and sent incrementally, which can be useful for real-time applications. """ - subqueries: Required[bool] - """Specify whether to include subqueries.""" + +class GraphQuestionParamsStreaming(GraphQuestionParamsBase): + stream: Required[Literal[True]] + """Determines whether the model's output should be streamed. + + If true, the output is generated and sent incrementally, which can be useful for + real-time applications. + """ + + +GraphQuestionParams = Union[GraphQuestionParamsNonStreaming, GraphQuestionParamsStreaming] diff --git a/tests/api_resources/test_graphs.py b/tests/api_resources/test_graphs.py index e243b436..1ce5771e 100644 --- a/tests/api_resources/test_graphs.py +++ b/tests/api_resources/test_graphs.py @@ -270,21 +270,21 @@ def test_path_params_add_file_to_graph(self, client: Writer) -> None: ) @parametrize - def test_method_question(self, client: Writer) -> None: + def test_method_question_overload_1(self, client: Writer) -> None: graph = client.graphs.question( graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], question="question", - stream=True, + stream=False, subqueries=True, ) assert_matches_type(Question, graph, path=["response"]) @parametrize - def test_raw_response_question(self, client: Writer) -> None: + def test_raw_response_question_overload_1(self, client: Writer) -> None: response = client.graphs.with_raw_response.question( graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], question="question", - stream=True, + stream=False, subqueries=True, ) @@ -294,11 +294,11 @@ def test_raw_response_question(self, client: Writer) -> None: assert_matches_type(Question, graph, path=["response"]) @parametrize - def test_streaming_response_question(self, client: Writer) -> None: + def test_streaming_response_question_overload_1(self, client: Writer) -> None: with client.graphs.with_streaming_response.question( graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], question="question", - stream=True, + stream=False, subqueries=True, ) as response: assert not response.is_closed @@ -309,6 +309,45 @@ def test_streaming_response_question(self, client: Writer) -> None: assert cast(Any, response.is_closed) is True + @parametrize + def test_method_question_overload_2(self, client: Writer) -> None: + graph_stream = client.graphs.question( + graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], + question="question", + stream=True, + subqueries=True, + ) + graph_stream.response.close() + + @parametrize + def test_raw_response_question_overload_2(self, client: Writer) -> None: + response = client.graphs.with_raw_response.question( + graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], + question="question", + stream=True, + subqueries=True, + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = response.parse() + stream.close() + + @parametrize + def test_streaming_response_question_overload_2(self, client: Writer) -> None: + with client.graphs.with_streaming_response.question( + graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], + question="question", + stream=True, + subqueries=True, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = response.parse() + stream.close() + + assert cast(Any, response.is_closed) is True + @parametrize def test_method_remove_file_from_graph(self, client: Writer) -> None: graph = client.graphs.remove_file_from_graph( @@ -605,21 +644,21 @@ async def test_path_params_add_file_to_graph(self, async_client: AsyncWriter) -> ) @parametrize - async def test_method_question(self, async_client: AsyncWriter) -> None: + async def test_method_question_overload_1(self, async_client: AsyncWriter) -> None: graph = await async_client.graphs.question( graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], question="question", - stream=True, + stream=False, subqueries=True, ) assert_matches_type(Question, graph, path=["response"]) @parametrize - async def test_raw_response_question(self, async_client: AsyncWriter) -> None: + async def test_raw_response_question_overload_1(self, async_client: AsyncWriter) -> None: response = await async_client.graphs.with_raw_response.question( graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], question="question", - stream=True, + stream=False, subqueries=True, ) @@ -629,11 +668,11 @@ async def test_raw_response_question(self, async_client: AsyncWriter) -> None: assert_matches_type(Question, graph, path=["response"]) @parametrize - async def test_streaming_response_question(self, async_client: AsyncWriter) -> None: + async def test_streaming_response_question_overload_1(self, async_client: AsyncWriter) -> None: async with async_client.graphs.with_streaming_response.question( graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], question="question", - stream=True, + stream=False, subqueries=True, ) as response: assert not response.is_closed @@ -644,6 +683,45 @@ async def test_streaming_response_question(self, async_client: AsyncWriter) -> N assert cast(Any, response.is_closed) is True + @parametrize + async def test_method_question_overload_2(self, async_client: AsyncWriter) -> None: + graph_stream = await async_client.graphs.question( + graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], + question="question", + stream=True, + subqueries=True, + ) + await graph_stream.response.aclose() + + @parametrize + async def test_raw_response_question_overload_2(self, async_client: AsyncWriter) -> None: + response = await async_client.graphs.with_raw_response.question( + graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], + question="question", + stream=True, + subqueries=True, + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = await response.parse() + await stream.close() + + @parametrize + async def test_streaming_response_question_overload_2(self, async_client: AsyncWriter) -> None: + async with async_client.graphs.with_streaming_response.question( + graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], + question="question", + stream=True, + subqueries=True, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = await response.parse() + await stream.close() + + assert cast(Any, response.is_closed) is True + @parametrize async def test_method_remove_file_from_graph(self, async_client: AsyncWriter) -> None: graph = await async_client.graphs.remove_file_from_graph( From 18103292d5693053cbb63931527edbf6786adb9b Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Fri, 8 Nov 2024 17:43:29 +0000 Subject: [PATCH 114/399] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index a10b14ec..9eee2711 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-9c49d7cc176d84ba91accd64aa4a8c91a44cdce83e9ea59da8b259fa19ccf9f6.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-c350270f059b9d4c51d9bca0c34c3e3018e544f5daaf2d18aacc55a0561573f8.yml From f1ad627bfe66a9b21b27eee640921a2ea0a94bce Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 8 Nov 2024 18:02:19 +0000 Subject: [PATCH 115/399] feat(api): manual updates (#111) --- api.md | 1 + src/writerai/resources/graphs.py | 17 +++++++++-------- src/writerai/types/__init__.py | 1 + src/writerai/types/question_streaming.py | 11 +++++++++++ 4 files changed, 22 insertions(+), 8 deletions(-) create mode 100644 src/writerai/types/question_streaming.py diff --git a/api.md b/api.md index eb822510..168ccd0b 100644 --- a/api.md +++ b/api.md @@ -54,6 +54,7 @@ Types: from writerai.types import ( Graph, Question, + QuestionStreaming, GraphCreateResponse, GraphUpdateResponse, GraphDeleteResponse, diff --git a/src/writerai/resources/graphs.py b/src/writerai/resources/graphs.py index 9d9df1bc..e0aa7d19 100644 --- a/src/writerai/resources/graphs.py +++ b/src/writerai/resources/graphs.py @@ -34,6 +34,7 @@ from ..types.graph import Graph from .._base_client import AsyncPaginator, make_request_options from ..types.question import Question +from ..types.question_streaming import QuestionStreaming from ..types.graph_create_response import GraphCreateResponse from ..types.graph_delete_response import GraphDeleteResponse from ..types.graph_update_response import GraphUpdateResponse @@ -369,7 +370,7 @@ def question( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Stream[Question]: + ) -> Stream[QuestionStreaming]: """ Ask a question to specified Knowledge Graphs. @@ -408,7 +409,7 @@ def question( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Question | Stream[Question]: + ) -> Question | Stream[QuestionStreaming]: """ Ask a question to specified Knowledge Graphs. @@ -447,7 +448,7 @@ def question( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Question | Stream[Question]: + ) -> Question | Stream[QuestionStreaming]: return self._post( "/v1/graphs/question", body=maybe_transform( @@ -464,7 +465,7 @@ def question( ), cast_to=Question, stream=stream or False, - stream_cls=Stream[Question], + stream_cls=Stream[QuestionStreaming], ) def remove_file_from_graph( @@ -833,7 +834,7 @@ async def question( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AsyncStream[Question]: + ) -> AsyncStream[QuestionStreaming]: """ Ask a question to specified Knowledge Graphs. @@ -872,7 +873,7 @@ async def question( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Question | AsyncStream[Question]: + ) -> Question | AsyncStream[QuestionStreaming]: """ Ask a question to specified Knowledge Graphs. @@ -911,7 +912,7 @@ async def question( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Question | AsyncStream[Question]: + ) -> Question | AsyncStream[QuestionStreaming]: return await self._post( "/v1/graphs/question", body=await async_maybe_transform( @@ -928,7 +929,7 @@ async def question( ), cast_to=Question, stream=stream or False, - stream_cls=AsyncStream[Question], + stream_cls=AsyncStream[QuestionStreaming], ) async def remove_file_from_graph( diff --git a/src/writerai/types/__init__.py b/src/writerai/types/__init__.py index d1a9a1e6..7fe3554e 100644 --- a/src/writerai/types/__init__.py +++ b/src/writerai/types/__init__.py @@ -13,6 +13,7 @@ from .file_retry_params import FileRetryParams as FileRetryParams from .graph_list_params import GraphListParams as GraphListParams from .file_upload_params import FileUploadParams as FileUploadParams +from .question_streaming import QuestionStreaming as QuestionStreaming from .file_retry_response import FileRetryResponse as FileRetryResponse from .graph_create_params import GraphCreateParams as GraphCreateParams from .graph_update_params import GraphUpdateParams as GraphUpdateParams diff --git a/src/writerai/types/question_streaming.py b/src/writerai/types/question_streaming.py new file mode 100644 index 00000000..9414914c --- /dev/null +++ b/src/writerai/types/question_streaming.py @@ -0,0 +1,11 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + + +from .._models import BaseModel +from .question import Question + +__all__ = ["QuestionStreaming"] + + +class QuestionStreaming(BaseModel): + data: Question From 98805d5e337e79db98b9eb2279735ac036240fc5 Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Fri, 8 Nov 2024 21:40:40 +0000 Subject: [PATCH 116/399] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 9eee2711..adc0248e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-c350270f059b9d4c51d9bca0c34c3e3018e544f5daaf2d18aacc55a0561573f8.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-0942adb3c35d7f4a10f2bd8a8ae32c03fb15266af751a9063663c2df3bc5a942.yml From dffe09f7e91ace93d730b7455dd71d2a1ae1bea1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 8 Nov 2024 22:16:00 +0000 Subject: [PATCH 117/399] feat(api): rename kg question and add text-to-graph (#112) --- .stats.yml | 4 +- api.md | 9 +- src/writerai/resources/graphs.py | 18 ++-- src/writerai/resources/tools/tools.py | 89 ++++++++++++++++++- src/writerai/types/__init__.py | 4 +- ...treaming.py => question_response_chunk.py} | 4 +- .../types/tool_text_to_graph_params.py | 12 +++ .../types/tool_text_to_graph_response.py | 15 ++++ tests/api_resources/test_tools.py | 63 +++++++++++++ 9 files changed, 201 insertions(+), 17 deletions(-) rename src/writerai/types/{question_streaming.py => question_response_chunk.py} (69%) create mode 100644 src/writerai/types/tool_text_to_graph_params.py create mode 100644 src/writerai/types/tool_text_to_graph_response.py diff --git a/.stats.yml b/.stats.yml index adc0248e..785904df 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ -configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-0942adb3c35d7f4a10f2bd8a8ae32c03fb15266af751a9063663c2df3bc5a942.yml +configured_endpoints: 22 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-82683f2fd5f8778a27960ebabda40d6dc4640bdfb77ac4ec7f173b8bf8076d3c.yml diff --git a/api.md b/api.md index 168ccd0b..aa7979a5 100644 --- a/api.md +++ b/api.md @@ -54,7 +54,7 @@ Types: from writerai.types import ( Graph, Question, - QuestionStreaming, + QuestionResponseChunk, GraphCreateResponse, GraphUpdateResponse, GraphDeleteResponse, @@ -95,13 +95,18 @@ Methods: Types: ```python -from writerai.types import ToolContextAwareSplittingResponse, ToolParsePdfResponse +from writerai.types import ( + ToolContextAwareSplittingResponse, + ToolParsePdfResponse, + ToolTextToGraphResponse, +) ``` Methods: - client.tools.context_aware_splitting(\*\*params) -> ToolContextAwareSplittingResponse - client.tools.parse_pdf(file_id, \*\*params) -> ToolParsePdfResponse +- client.tools.text_to_graph(\*\*params) -> ToolTextToGraphResponse ## Comprehend diff --git a/src/writerai/resources/graphs.py b/src/writerai/resources/graphs.py index e0aa7d19..ed4e639c 100644 --- a/src/writerai/resources/graphs.py +++ b/src/writerai/resources/graphs.py @@ -34,10 +34,10 @@ from ..types.graph import Graph from .._base_client import AsyncPaginator, make_request_options from ..types.question import Question -from ..types.question_streaming import QuestionStreaming from ..types.graph_create_response import GraphCreateResponse from ..types.graph_delete_response import GraphDeleteResponse from ..types.graph_update_response import GraphUpdateResponse +from ..types.question_response_chunk import QuestionResponseChunk from ..types.graph_remove_file_from_graph_response import GraphRemoveFileFromGraphResponse __all__ = ["GraphsResource", "AsyncGraphsResource"] @@ -370,7 +370,7 @@ def question( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Stream[QuestionStreaming]: + ) -> Stream[QuestionResponseChunk]: """ Ask a question to specified Knowledge Graphs. @@ -409,7 +409,7 @@ def question( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Question | Stream[QuestionStreaming]: + ) -> Question | Stream[QuestionResponseChunk]: """ Ask a question to specified Knowledge Graphs. @@ -448,7 +448,7 @@ def question( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Question | Stream[QuestionStreaming]: + ) -> Question | Stream[QuestionResponseChunk]: return self._post( "/v1/graphs/question", body=maybe_transform( @@ -465,7 +465,7 @@ def question( ), cast_to=Question, stream=stream or False, - stream_cls=Stream[QuestionStreaming], + stream_cls=Stream[QuestionResponseChunk], ) def remove_file_from_graph( @@ -834,7 +834,7 @@ async def question( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AsyncStream[QuestionStreaming]: + ) -> AsyncStream[QuestionResponseChunk]: """ Ask a question to specified Knowledge Graphs. @@ -873,7 +873,7 @@ async def question( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Question | AsyncStream[QuestionStreaming]: + ) -> Question | AsyncStream[QuestionResponseChunk]: """ Ask a question to specified Knowledge Graphs. @@ -912,7 +912,7 @@ async def question( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Question | AsyncStream[QuestionStreaming]: + ) -> Question | AsyncStream[QuestionResponseChunk]: return await self._post( "/v1/graphs/question", body=await async_maybe_transform( @@ -929,7 +929,7 @@ async def question( ), cast_to=Question, stream=stream or False, - stream_cls=AsyncStream[QuestionStreaming], + stream_cls=AsyncStream[QuestionResponseChunk], ) async def remove_file_from_graph( diff --git a/src/writerai/resources/tools/tools.py b/src/writerai/resources/tools/tools.py index 8809cf20..cacf8abb 100644 --- a/src/writerai/resources/tools/tools.py +++ b/src/writerai/resources/tools/tools.py @@ -6,7 +6,11 @@ import httpx -from ...types import tool_parse_pdf_params, tool_context_aware_splitting_params +from ...types import ( + tool_parse_pdf_params, + tool_text_to_graph_params, + tool_context_aware_splitting_params, +) from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven from ..._utils import ( maybe_transform, @@ -30,6 +34,7 @@ ) from ..._base_client import make_request_options from ...types.tool_parse_pdf_response import ToolParsePdfResponse +from ...types.tool_text_to_graph_response import ToolTextToGraphResponse from ...types.tool_context_aware_splitting_response import ToolContextAwareSplittingResponse __all__ = ["ToolsResource", "AsyncToolsResource"] @@ -142,6 +147,41 @@ def parse_pdf( cast_to=ToolParsePdfResponse, ) + def text_to_graph( + self, + *, + text: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ToolTextToGraphResponse: + """ + Performs name entity recognition on the supplied text accepting a maximum of + 35000 words. + + Args: + text: The text to be converted into a graph structure. Maximum of 35000 words. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/v1/tools/text-to-graph", + body=maybe_transform({"text": text}, tool_text_to_graph_params.ToolTextToGraphParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ToolTextToGraphResponse, + ) + class AsyncToolsResource(AsyncAPIResource): @cached_property @@ -250,6 +290,41 @@ async def parse_pdf( cast_to=ToolParsePdfResponse, ) + async def text_to_graph( + self, + *, + text: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ToolTextToGraphResponse: + """ + Performs name entity recognition on the supplied text accepting a maximum of + 35000 words. + + Args: + text: The text to be converted into a graph structure. Maximum of 35000 words. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/v1/tools/text-to-graph", + body=await async_maybe_transform({"text": text}, tool_text_to_graph_params.ToolTextToGraphParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ToolTextToGraphResponse, + ) + class ToolsResourceWithRawResponse: def __init__(self, tools: ToolsResource) -> None: @@ -261,6 +336,9 @@ def __init__(self, tools: ToolsResource) -> None: self.parse_pdf = to_raw_response_wrapper( tools.parse_pdf, ) + self.text_to_graph = to_raw_response_wrapper( + tools.text_to_graph, + ) @cached_property def comprehend(self) -> ComprehendResourceWithRawResponse: @@ -277,6 +355,9 @@ def __init__(self, tools: AsyncToolsResource) -> None: self.parse_pdf = async_to_raw_response_wrapper( tools.parse_pdf, ) + self.text_to_graph = async_to_raw_response_wrapper( + tools.text_to_graph, + ) @cached_property def comprehend(self) -> AsyncComprehendResourceWithRawResponse: @@ -293,6 +374,9 @@ def __init__(self, tools: ToolsResource) -> None: self.parse_pdf = to_streamed_response_wrapper( tools.parse_pdf, ) + self.text_to_graph = to_streamed_response_wrapper( + tools.text_to_graph, + ) @cached_property def comprehend(self) -> ComprehendResourceWithStreamingResponse: @@ -309,6 +393,9 @@ def __init__(self, tools: AsyncToolsResource) -> None: self.parse_pdf = async_to_streamed_response_wrapper( tools.parse_pdf, ) + self.text_to_graph = async_to_streamed_response_wrapper( + tools.text_to_graph, + ) @cached_property def comprehend(self) -> AsyncComprehendResourceWithStreamingResponse: diff --git a/src/writerai/types/__init__.py b/src/writerai/types/__init__.py index 7fe3554e..3498dc67 100644 --- a/src/writerai/types/__init__.py +++ b/src/writerai/types/__init__.py @@ -13,7 +13,6 @@ from .file_retry_params import FileRetryParams as FileRetryParams from .graph_list_params import GraphListParams as GraphListParams from .file_upload_params import FileUploadParams as FileUploadParams -from .question_streaming import QuestionStreaming as QuestionStreaming from .file_retry_response import FileRetryResponse as FileRetryResponse from .graph_create_params import GraphCreateParams as GraphCreateParams from .graph_update_params import GraphUpdateParams as GraphUpdateParams @@ -25,8 +24,11 @@ from .graph_question_params import GraphQuestionParams as GraphQuestionParams from .graph_update_response import GraphUpdateResponse as GraphUpdateResponse from .tool_parse_pdf_params import ToolParsePdfParams as ToolParsePdfParams +from .question_response_chunk import QuestionResponseChunk as QuestionResponseChunk from .tool_parse_pdf_response import ToolParsePdfResponse as ToolParsePdfResponse from .completion_create_params import CompletionCreateParams as CompletionCreateParams +from .tool_text_to_graph_params import ToolTextToGraphParams as ToolTextToGraphParams +from .tool_text_to_graph_response import ToolTextToGraphResponse as ToolTextToGraphResponse from .graph_add_file_to_graph_params import GraphAddFileToGraphParams as GraphAddFileToGraphParams from .application_generate_content_params import ApplicationGenerateContentParams as ApplicationGenerateContentParams from .tool_context_aware_splitting_params import ToolContextAwareSplittingParams as ToolContextAwareSplittingParams diff --git a/src/writerai/types/question_streaming.py b/src/writerai/types/question_response_chunk.py similarity index 69% rename from src/writerai/types/question_streaming.py rename to src/writerai/types/question_response_chunk.py index 9414914c..147a00f3 100644 --- a/src/writerai/types/question_streaming.py +++ b/src/writerai/types/question_response_chunk.py @@ -4,8 +4,8 @@ from .._models import BaseModel from .question import Question -__all__ = ["QuestionStreaming"] +__all__ = ["QuestionResponseChunk"] -class QuestionStreaming(BaseModel): +class QuestionResponseChunk(BaseModel): data: Question diff --git a/src/writerai/types/tool_text_to_graph_params.py b/src/writerai/types/tool_text_to_graph_params.py new file mode 100644 index 00000000..102fbfed --- /dev/null +++ b/src/writerai/types/tool_text_to_graph_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["ToolTextToGraphParams"] + + +class ToolTextToGraphParams(TypedDict, total=False): + text: Required[str] + """The text to be converted into a graph structure. Maximum of 35000 words.""" diff --git a/src/writerai/types/tool_text_to_graph_response.py b/src/writerai/types/tool_text_to_graph_response.py new file mode 100644 index 00000000..58f87bd5 --- /dev/null +++ b/src/writerai/types/tool_text_to_graph_response.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List + +from .._models import BaseModel + +__all__ = ["ToolTextToGraphResponse"] + + +class ToolTextToGraphResponse(BaseModel): + graph: List[List[str]] + """ + The graph structure generated from the input text, represented by a string array + of entities and relationships. + """ diff --git a/tests/api_resources/test_tools.py b/tests/api_resources/test_tools.py index 0fa0fdc0..029cb1c5 100644 --- a/tests/api_resources/test_tools.py +++ b/tests/api_resources/test_tools.py @@ -11,6 +11,7 @@ from tests.utils import assert_matches_type from writerai.types import ( ToolParsePdfResponse, + ToolTextToGraphResponse, ToolContextAwareSplittingResponse, ) @@ -96,6 +97,37 @@ def test_path_params_parse_pdf(self, client: Writer) -> None: format="text", ) + @parametrize + def test_method_text_to_graph(self, client: Writer) -> None: + tool = client.tools.text_to_graph( + text="text", + ) + assert_matches_type(ToolTextToGraphResponse, tool, path=["response"]) + + @parametrize + def test_raw_response_text_to_graph(self, client: Writer) -> None: + response = client.tools.with_raw_response.text_to_graph( + text="text", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + tool = response.parse() + assert_matches_type(ToolTextToGraphResponse, tool, path=["response"]) + + @parametrize + def test_streaming_response_text_to_graph(self, client: Writer) -> None: + with client.tools.with_streaming_response.text_to_graph( + text="text", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + tool = response.parse() + assert_matches_type(ToolTextToGraphResponse, tool, path=["response"]) + + assert cast(Any, response.is_closed) is True + class TestAsyncTools: parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) @@ -175,3 +207,34 @@ async def test_path_params_parse_pdf(self, async_client: AsyncWriter) -> None: file_id="", format="text", ) + + @parametrize + async def test_method_text_to_graph(self, async_client: AsyncWriter) -> None: + tool = await async_client.tools.text_to_graph( + text="text", + ) + assert_matches_type(ToolTextToGraphResponse, tool, path=["response"]) + + @parametrize + async def test_raw_response_text_to_graph(self, async_client: AsyncWriter) -> None: + response = await async_client.tools.with_raw_response.text_to_graph( + text="text", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + tool = await response.parse() + assert_matches_type(ToolTextToGraphResponse, tool, path=["response"]) + + @parametrize + async def test_streaming_response_text_to_graph(self, async_client: AsyncWriter) -> None: + async with async_client.tools.with_streaming_response.text_to_graph( + text="text", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + tool = await response.parse() + assert_matches_type(ToolTextToGraphResponse, tool, path=["response"]) + + assert cast(Any, response.is_closed) is True From e01898ec81294783fd39c8cc8f66b6758df4bdb4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 12 Nov 2024 03:07:38 +0000 Subject: [PATCH 118/399] chore: rebuild project due to codegen change (#113) --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 16aa2f68..376c6bd8 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,7 @@ import os from writerai import Writer client = Writer( - # This is the default and can be omitted - api_key=os.environ.get("WRITER_API_KEY"), + api_key=os.environ.get("WRITER_API_KEY"), # This is the default and can be omitted ) chat = client.chat.chat( @@ -54,8 +53,7 @@ import asyncio from writerai import AsyncWriter client = AsyncWriter( - # This is the default and can be omitted - api_key=os.environ.get("WRITER_API_KEY"), + api_key=os.environ.get("WRITER_API_KEY"), # This is the default and can be omitted ) From dcd93b4782e3e4986579729aa0fa3723921a3607 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 12 Nov 2024 17:43:11 +0000 Subject: [PATCH 119/399] feat(api): manual updates (#114) --- .stats.yml | 2 +- api.md | 7 +- src/writerai/_utils/_transform.py | 5 ++ src/writerai/resources/tools/tools.py | 89 +------------------ src/writerai/types/__init__.py | 2 - .../types/tool_text_to_graph_params.py | 12 --- .../types/tool_text_to_graph_response.py | 15 ---- tests/api_resources/test_tools.py | 63 ------------- 8 files changed, 8 insertions(+), 187 deletions(-) delete mode 100644 src/writerai/types/tool_text_to_graph_params.py delete mode 100644 src/writerai/types/tool_text_to_graph_response.py diff --git a/.stats.yml b/.stats.yml index 785904df..40f1ba49 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ -configured_endpoints: 22 +configured_endpoints: 21 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-82683f2fd5f8778a27960ebabda40d6dc4640bdfb77ac4ec7f173b8bf8076d3c.yml diff --git a/api.md b/api.md index aa7979a5..a86244a6 100644 --- a/api.md +++ b/api.md @@ -95,18 +95,13 @@ Methods: Types: ```python -from writerai.types import ( - ToolContextAwareSplittingResponse, - ToolParsePdfResponse, - ToolTextToGraphResponse, -) +from writerai.types import ToolContextAwareSplittingResponse, ToolParsePdfResponse ``` Methods: - client.tools.context_aware_splitting(\*\*params) -> ToolContextAwareSplittingResponse - client.tools.parse_pdf(file_id, \*\*params) -> ToolParsePdfResponse -- client.tools.text_to_graph(\*\*params) -> ToolTextToGraphResponse ## Comprehend diff --git a/src/writerai/_utils/_transform.py b/src/writerai/_utils/_transform.py index d7c05345..a6b62cad 100644 --- a/src/writerai/_utils/_transform.py +++ b/src/writerai/_utils/_transform.py @@ -316,6 +316,11 @@ async def _async_transform_recursive( # Iterable[T] or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str)) ): + # dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually + # intended as an iterable, so we don't transform it. + if isinstance(data, dict): + return cast(object, data) + inner_type = extract_type_arg(stripped_type, 0) return [await _async_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data] diff --git a/src/writerai/resources/tools/tools.py b/src/writerai/resources/tools/tools.py index cacf8abb..8809cf20 100644 --- a/src/writerai/resources/tools/tools.py +++ b/src/writerai/resources/tools/tools.py @@ -6,11 +6,7 @@ import httpx -from ...types import ( - tool_parse_pdf_params, - tool_text_to_graph_params, - tool_context_aware_splitting_params, -) +from ...types import tool_parse_pdf_params, tool_context_aware_splitting_params from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven from ..._utils import ( maybe_transform, @@ -34,7 +30,6 @@ ) from ..._base_client import make_request_options from ...types.tool_parse_pdf_response import ToolParsePdfResponse -from ...types.tool_text_to_graph_response import ToolTextToGraphResponse from ...types.tool_context_aware_splitting_response import ToolContextAwareSplittingResponse __all__ = ["ToolsResource", "AsyncToolsResource"] @@ -147,41 +142,6 @@ def parse_pdf( cast_to=ToolParsePdfResponse, ) - def text_to_graph( - self, - *, - text: str, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> ToolTextToGraphResponse: - """ - Performs name entity recognition on the supplied text accepting a maximum of - 35000 words. - - Args: - text: The text to be converted into a graph structure. Maximum of 35000 words. - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return self._post( - "/v1/tools/text-to-graph", - body=maybe_transform({"text": text}, tool_text_to_graph_params.ToolTextToGraphParams), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ToolTextToGraphResponse, - ) - class AsyncToolsResource(AsyncAPIResource): @cached_property @@ -290,41 +250,6 @@ async def parse_pdf( cast_to=ToolParsePdfResponse, ) - async def text_to_graph( - self, - *, - text: str, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> ToolTextToGraphResponse: - """ - Performs name entity recognition on the supplied text accepting a maximum of - 35000 words. - - Args: - text: The text to be converted into a graph structure. Maximum of 35000 words. - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return await self._post( - "/v1/tools/text-to-graph", - body=await async_maybe_transform({"text": text}, tool_text_to_graph_params.ToolTextToGraphParams), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ToolTextToGraphResponse, - ) - class ToolsResourceWithRawResponse: def __init__(self, tools: ToolsResource) -> None: @@ -336,9 +261,6 @@ def __init__(self, tools: ToolsResource) -> None: self.parse_pdf = to_raw_response_wrapper( tools.parse_pdf, ) - self.text_to_graph = to_raw_response_wrapper( - tools.text_to_graph, - ) @cached_property def comprehend(self) -> ComprehendResourceWithRawResponse: @@ -355,9 +277,6 @@ def __init__(self, tools: AsyncToolsResource) -> None: self.parse_pdf = async_to_raw_response_wrapper( tools.parse_pdf, ) - self.text_to_graph = async_to_raw_response_wrapper( - tools.text_to_graph, - ) @cached_property def comprehend(self) -> AsyncComprehendResourceWithRawResponse: @@ -374,9 +293,6 @@ def __init__(self, tools: ToolsResource) -> None: self.parse_pdf = to_streamed_response_wrapper( tools.parse_pdf, ) - self.text_to_graph = to_streamed_response_wrapper( - tools.text_to_graph, - ) @cached_property def comprehend(self) -> ComprehendResourceWithStreamingResponse: @@ -393,9 +309,6 @@ def __init__(self, tools: AsyncToolsResource) -> None: self.parse_pdf = async_to_streamed_response_wrapper( tools.parse_pdf, ) - self.text_to_graph = async_to_streamed_response_wrapper( - tools.text_to_graph, - ) @cached_property def comprehend(self) -> AsyncComprehendResourceWithStreamingResponse: diff --git a/src/writerai/types/__init__.py b/src/writerai/types/__init__.py index 3498dc67..af6ad6c7 100644 --- a/src/writerai/types/__init__.py +++ b/src/writerai/types/__init__.py @@ -27,8 +27,6 @@ from .question_response_chunk import QuestionResponseChunk as QuestionResponseChunk from .tool_parse_pdf_response import ToolParsePdfResponse as ToolParsePdfResponse from .completion_create_params import CompletionCreateParams as CompletionCreateParams -from .tool_text_to_graph_params import ToolTextToGraphParams as ToolTextToGraphParams -from .tool_text_to_graph_response import ToolTextToGraphResponse as ToolTextToGraphResponse from .graph_add_file_to_graph_params import GraphAddFileToGraphParams as GraphAddFileToGraphParams from .application_generate_content_params import ApplicationGenerateContentParams as ApplicationGenerateContentParams from .tool_context_aware_splitting_params import ToolContextAwareSplittingParams as ToolContextAwareSplittingParams diff --git a/src/writerai/types/tool_text_to_graph_params.py b/src/writerai/types/tool_text_to_graph_params.py deleted file mode 100644 index 102fbfed..00000000 --- a/src/writerai/types/tool_text_to_graph_params.py +++ /dev/null @@ -1,12 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Required, TypedDict - -__all__ = ["ToolTextToGraphParams"] - - -class ToolTextToGraphParams(TypedDict, total=False): - text: Required[str] - """The text to be converted into a graph structure. Maximum of 35000 words.""" diff --git a/src/writerai/types/tool_text_to_graph_response.py b/src/writerai/types/tool_text_to_graph_response.py deleted file mode 100644 index 58f87bd5..00000000 --- a/src/writerai/types/tool_text_to_graph_response.py +++ /dev/null @@ -1,15 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List - -from .._models import BaseModel - -__all__ = ["ToolTextToGraphResponse"] - - -class ToolTextToGraphResponse(BaseModel): - graph: List[List[str]] - """ - The graph structure generated from the input text, represented by a string array - of entities and relationships. - """ diff --git a/tests/api_resources/test_tools.py b/tests/api_resources/test_tools.py index 029cb1c5..0fa0fdc0 100644 --- a/tests/api_resources/test_tools.py +++ b/tests/api_resources/test_tools.py @@ -11,7 +11,6 @@ from tests.utils import assert_matches_type from writerai.types import ( ToolParsePdfResponse, - ToolTextToGraphResponse, ToolContextAwareSplittingResponse, ) @@ -97,37 +96,6 @@ def test_path_params_parse_pdf(self, client: Writer) -> None: format="text", ) - @parametrize - def test_method_text_to_graph(self, client: Writer) -> None: - tool = client.tools.text_to_graph( - text="text", - ) - assert_matches_type(ToolTextToGraphResponse, tool, path=["response"]) - - @parametrize - def test_raw_response_text_to_graph(self, client: Writer) -> None: - response = client.tools.with_raw_response.text_to_graph( - text="text", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - tool = response.parse() - assert_matches_type(ToolTextToGraphResponse, tool, path=["response"]) - - @parametrize - def test_streaming_response_text_to_graph(self, client: Writer) -> None: - with client.tools.with_streaming_response.text_to_graph( - text="text", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - tool = response.parse() - assert_matches_type(ToolTextToGraphResponse, tool, path=["response"]) - - assert cast(Any, response.is_closed) is True - class TestAsyncTools: parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) @@ -207,34 +175,3 @@ async def test_path_params_parse_pdf(self, async_client: AsyncWriter) -> None: file_id="", format="text", ) - - @parametrize - async def test_method_text_to_graph(self, async_client: AsyncWriter) -> None: - tool = await async_client.tools.text_to_graph( - text="text", - ) - assert_matches_type(ToolTextToGraphResponse, tool, path=["response"]) - - @parametrize - async def test_raw_response_text_to_graph(self, async_client: AsyncWriter) -> None: - response = await async_client.tools.with_raw_response.text_to_graph( - text="text", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - tool = await response.parse() - assert_matches_type(ToolTextToGraphResponse, tool, path=["response"]) - - @parametrize - async def test_streaming_response_text_to_graph(self, async_client: AsyncWriter) -> None: - async with async_client.tools.with_streaming_response.text_to_graph( - text="text", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - tool = await response.parse() - assert_matches_type(ToolTextToGraphResponse, tool, path=["response"]) - - assert cast(Any, response.is_closed) is True From 2503901250bca88b388d213ac4455bc4b478a74d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 14 Nov 2024 14:22:54 +0000 Subject: [PATCH 120/399] chore(internal): version bump (#115) --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d0ab6645..2a8f4ffd 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.2.0" + ".": "1.3.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 046e4fb4..07bd23c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "1.2.0" +version = "1.3.0" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index 2c354a52..b6bf1102 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "1.2.0" # x-release-please-version +__version__ = "1.3.0" # x-release-please-version From e005b3809d032203ee82d3d3a5a5f6bd61718b35 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 11:37:20 +0000 Subject: [PATCH 121/399] chore: rebuild project due to codegen change (#116) --- tests/api_resources/test_applications.py | 96 ++----- tests/api_resources/test_chat.py | 312 ++--------------------- tests/api_resources/test_files.py | 36 +-- 3 files changed, 42 insertions(+), 402 deletions(-) diff --git a/tests/api_resources/test_applications.py b/tests/api_resources/test_applications.py index f1063ba3..cdf9faa2 100644 --- a/tests/api_resources/test_applications.py +++ b/tests/api_resources/test_applications.py @@ -24,16 +24,8 @@ def test_method_generate_content(self, client: Writer) -> None: inputs=[ { "id": "id", - "value": ["string", "string", "string"], - }, - { - "id": "id", - "value": ["string", "string", "string"], - }, - { - "id": "id", - "value": ["string", "string", "string"], - }, + "value": ["string"], + } ], ) assert_matches_type(ApplicationGenerateContentResponse, application, path=["response"]) @@ -45,16 +37,8 @@ def test_raw_response_generate_content(self, client: Writer) -> None: inputs=[ { "id": "id", - "value": ["string", "string", "string"], - }, - { - "id": "id", - "value": ["string", "string", "string"], - }, - { - "id": "id", - "value": ["string", "string", "string"], - }, + "value": ["string"], + } ], ) @@ -70,16 +54,8 @@ def test_streaming_response_generate_content(self, client: Writer) -> None: inputs=[ { "id": "id", - "value": ["string", "string", "string"], - }, - { - "id": "id", - "value": ["string", "string", "string"], - }, - { - "id": "id", - "value": ["string", "string", "string"], - }, + "value": ["string"], + } ], ) as response: assert not response.is_closed @@ -98,16 +74,8 @@ def test_path_params_generate_content(self, client: Writer) -> None: inputs=[ { "id": "id", - "value": ["string", "string", "string"], - }, - { - "id": "id", - "value": ["string", "string", "string"], - }, - { - "id": "id", - "value": ["string", "string", "string"], - }, + "value": ["string"], + } ], ) @@ -122,16 +90,8 @@ async def test_method_generate_content(self, async_client: AsyncWriter) -> None: inputs=[ { "id": "id", - "value": ["string", "string", "string"], - }, - { - "id": "id", - "value": ["string", "string", "string"], - }, - { - "id": "id", - "value": ["string", "string", "string"], - }, + "value": ["string"], + } ], ) assert_matches_type(ApplicationGenerateContentResponse, application, path=["response"]) @@ -143,16 +103,8 @@ async def test_raw_response_generate_content(self, async_client: AsyncWriter) -> inputs=[ { "id": "id", - "value": ["string", "string", "string"], - }, - { - "id": "id", - "value": ["string", "string", "string"], - }, - { - "id": "id", - "value": ["string", "string", "string"], - }, + "value": ["string"], + } ], ) @@ -168,16 +120,8 @@ async def test_streaming_response_generate_content(self, async_client: AsyncWrit inputs=[ { "id": "id", - "value": ["string", "string", "string"], - }, - { - "id": "id", - "value": ["string", "string", "string"], - }, - { - "id": "id", - "value": ["string", "string", "string"], - }, + "value": ["string"], + } ], ) as response: assert not response.is_closed @@ -196,15 +140,7 @@ async def test_path_params_generate_content(self, async_client: AsyncWriter) -> inputs=[ { "id": "id", - "value": ["string", "string", "string"], - }, - { - "id": "id", - "value": ["string", "string", "string"], - }, - { - "id": "id", - "value": ["string", "string", "string"], - }, + "value": ["string"], + } ], ) diff --git a/tests/api_resources/test_chat.py b/tests/api_resources/test_chat.py index 0906891a..03c27d4d 100644 --- a/tests/api_resources/test_chat.py +++ b/tests/api_resources/test_chat.py @@ -37,15 +37,7 @@ def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: { "file_id": "file_id", "snippet": "snippet", - }, - { - "file_id": "file_id", - "snippet": "snippet", - }, - { - "file_id": "file_id", - "snippet": "snippet", - }, + } ], "status": "processing", "subqueries": [ @@ -56,53 +48,9 @@ def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: { "file_id": "file_id", "snippet": "snippet", - }, - { - "file_id": "file_id", - "snippet": "snippet", - }, - { - "file_id": "file_id", - "snippet": "snippet", - }, + } ], - }, - { - "answer": "answer", - "query": "query", - "sources": [ - { - "file_id": "file_id", - "snippet": "snippet", - }, - { - "file_id": "file_id", - "snippet": "snippet", - }, - { - "file_id": "file_id", - "snippet": "snippet", - }, - ], - }, - { - "answer": "answer", - "query": "query", - "sources": [ - { - "file_id": "file_id", - "snippet": "snippet", - }, - { - "file_id": "file_id", - "snippet": "snippet", - }, - { - "file_id": "file_id", - "snippet": "snippet", - }, - ], - }, + } ], }, "name": "name", @@ -125,7 +73,7 @@ def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: logprobs=True, max_tokens=0, n=0, - stop=["string", "string", "string"], + stop=["string"], stream=False, stream_options={"include_usage": True}, temperature=0, @@ -138,23 +86,7 @@ def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: "parameters": {"foo": "bar"}, }, "type": "function", - }, - { - "function": { - "name": "name", - "description": "description", - "parameters": {"foo": "bar"}, - }, - "type": "function", - }, - { - "function": { - "name": "name", - "description": "description", - "parameters": {"foo": "bar"}, - }, - "type": "function", - }, + } ], top_p=0, ) @@ -207,15 +139,7 @@ def test_method_chat_with_all_params_overload_2(self, client: Writer) -> None: { "file_id": "file_id", "snippet": "snippet", - }, - { - "file_id": "file_id", - "snippet": "snippet", - }, - { - "file_id": "file_id", - "snippet": "snippet", - }, + } ], "status": "processing", "subqueries": [ @@ -226,53 +150,9 @@ def test_method_chat_with_all_params_overload_2(self, client: Writer) -> None: { "file_id": "file_id", "snippet": "snippet", - }, - { - "file_id": "file_id", - "snippet": "snippet", - }, - { - "file_id": "file_id", - "snippet": "snippet", - }, + } ], - }, - { - "answer": "answer", - "query": "query", - "sources": [ - { - "file_id": "file_id", - "snippet": "snippet", - }, - { - "file_id": "file_id", - "snippet": "snippet", - }, - { - "file_id": "file_id", - "snippet": "snippet", - }, - ], - }, - { - "answer": "answer", - "query": "query", - "sources": [ - { - "file_id": "file_id", - "snippet": "snippet", - }, - { - "file_id": "file_id", - "snippet": "snippet", - }, - { - "file_id": "file_id", - "snippet": "snippet", - }, - ], - }, + } ], }, "name": "name", @@ -296,7 +176,7 @@ def test_method_chat_with_all_params_overload_2(self, client: Writer) -> None: logprobs=True, max_tokens=0, n=0, - stop=["string", "string", "string"], + stop=["string"], stream_options={"include_usage": True}, temperature=0, tool_choice={"value": "none"}, @@ -308,23 +188,7 @@ def test_method_chat_with_all_params_overload_2(self, client: Writer) -> None: "parameters": {"foo": "bar"}, }, "type": "function", - }, - { - "function": { - "name": "name", - "description": "description", - "parameters": {"foo": "bar"}, - }, - "type": "function", - }, - { - "function": { - "name": "name", - "description": "description", - "parameters": {"foo": "bar"}, - }, - "type": "function", - }, + } ], top_p=0, ) @@ -381,15 +245,7 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW { "file_id": "file_id", "snippet": "snippet", - }, - { - "file_id": "file_id", - "snippet": "snippet", - }, - { - "file_id": "file_id", - "snippet": "snippet", - }, + } ], "status": "processing", "subqueries": [ @@ -400,53 +256,9 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW { "file_id": "file_id", "snippet": "snippet", - }, - { - "file_id": "file_id", - "snippet": "snippet", - }, - { - "file_id": "file_id", - "snippet": "snippet", - }, + } ], - }, - { - "answer": "answer", - "query": "query", - "sources": [ - { - "file_id": "file_id", - "snippet": "snippet", - }, - { - "file_id": "file_id", - "snippet": "snippet", - }, - { - "file_id": "file_id", - "snippet": "snippet", - }, - ], - }, - { - "answer": "answer", - "query": "query", - "sources": [ - { - "file_id": "file_id", - "snippet": "snippet", - }, - { - "file_id": "file_id", - "snippet": "snippet", - }, - { - "file_id": "file_id", - "snippet": "snippet", - }, - ], - }, + } ], }, "name": "name", @@ -469,7 +281,7 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW logprobs=True, max_tokens=0, n=0, - stop=["string", "string", "string"], + stop=["string"], stream=False, stream_options={"include_usage": True}, temperature=0, @@ -482,23 +294,7 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW "parameters": {"foo": "bar"}, }, "type": "function", - }, - { - "function": { - "name": "name", - "description": "description", - "parameters": {"foo": "bar"}, - }, - "type": "function", - }, - { - "function": { - "name": "name", - "description": "description", - "parameters": {"foo": "bar"}, - }, - "type": "function", - }, + } ], top_p=0, ) @@ -551,15 +347,7 @@ async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncW { "file_id": "file_id", "snippet": "snippet", - }, - { - "file_id": "file_id", - "snippet": "snippet", - }, - { - "file_id": "file_id", - "snippet": "snippet", - }, + } ], "status": "processing", "subqueries": [ @@ -570,53 +358,9 @@ async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncW { "file_id": "file_id", "snippet": "snippet", - }, - { - "file_id": "file_id", - "snippet": "snippet", - }, - { - "file_id": "file_id", - "snippet": "snippet", - }, + } ], - }, - { - "answer": "answer", - "query": "query", - "sources": [ - { - "file_id": "file_id", - "snippet": "snippet", - }, - { - "file_id": "file_id", - "snippet": "snippet", - }, - { - "file_id": "file_id", - "snippet": "snippet", - }, - ], - }, - { - "answer": "answer", - "query": "query", - "sources": [ - { - "file_id": "file_id", - "snippet": "snippet", - }, - { - "file_id": "file_id", - "snippet": "snippet", - }, - { - "file_id": "file_id", - "snippet": "snippet", - }, - ], - }, + } ], }, "name": "name", @@ -640,7 +384,7 @@ async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncW logprobs=True, max_tokens=0, n=0, - stop=["string", "string", "string"], + stop=["string"], stream_options={"include_usage": True}, temperature=0, tool_choice={"value": "none"}, @@ -652,23 +396,7 @@ async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncW "parameters": {"foo": "bar"}, }, "type": "function", - }, - { - "function": { - "name": "name", - "description": "description", - "parameters": {"foo": "bar"}, - }, - "type": "function", - }, - { - "function": { - "name": "name", - "description": "description", - "parameters": {"foo": "bar"}, - }, - "type": "function", - }, + } ], top_p=0, ) diff --git a/tests/api_resources/test_files.py b/tests/api_resources/test_files.py index 6afa628d..90cb5c04 100644 --- a/tests/api_resources/test_files.py +++ b/tests/api_resources/test_files.py @@ -200,22 +200,14 @@ def test_path_params_download(self, client: Writer) -> None: @parametrize def test_method_retry(self, client: Writer) -> None: file = client.files.retry( - file_ids=[ - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ], + file_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], ) assert_matches_type(FileRetryResponse, file, path=["response"]) @parametrize def test_raw_response_retry(self, client: Writer) -> None: response = client.files.with_raw_response.retry( - file_ids=[ - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ], + file_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], ) assert response.is_closed is True @@ -226,11 +218,7 @@ def test_raw_response_retry(self, client: Writer) -> None: @parametrize def test_streaming_response_retry(self, client: Writer) -> None: with client.files.with_streaming_response.retry( - file_ids=[ - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ], + file_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -451,22 +439,14 @@ async def test_path_params_download(self, async_client: AsyncWriter) -> None: @parametrize async def test_method_retry(self, async_client: AsyncWriter) -> None: file = await async_client.files.retry( - file_ids=[ - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ], + file_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], ) assert_matches_type(FileRetryResponse, file, path=["response"]) @parametrize async def test_raw_response_retry(self, async_client: AsyncWriter) -> None: response = await async_client.files.with_raw_response.retry( - file_ids=[ - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ], + file_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], ) assert response.is_closed is True @@ -477,11 +457,7 @@ async def test_raw_response_retry(self, async_client: AsyncWriter) -> None: @parametrize async def test_streaming_response_retry(self, async_client: AsyncWriter) -> None: async with async_client.files.with_streaming_response.retry( - file_ids=[ - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ], + file_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" From 29923cd4e3fee801e132b2a971d5bdc3f7e171bc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 13:10:59 +0000 Subject: [PATCH 122/399] chore: rebuild project due to codegen change (#118) --- pyproject.toml | 1 + requirements-dev.lock | 1 + src/writerai/_utils/_sync.py | 90 ++++++++++++++++-------------------- tests/test_client.py | 38 +++++++++++++++ 4 files changed, 80 insertions(+), 50 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 07bd23c3..8abf6ffb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,7 @@ dev-dependencies = [ "dirty-equals>=0.6.0", "importlib-metadata>=6.7.0", "rich>=13.7.1", + "nest_asyncio==1.6.0" ] [tool.rye.scripts] diff --git a/requirements-dev.lock b/requirements-dev.lock index 364ec7fc..0bd4c5da 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -51,6 +51,7 @@ mdurl==0.1.2 mypy==1.13.0 mypy-extensions==1.0.0 # via mypy +nest-asyncio==1.6.0 nodeenv==1.8.0 # via pyright nox==2023.4.22 diff --git a/src/writerai/_utils/_sync.py b/src/writerai/_utils/_sync.py index d0d81033..8b3aaf2b 100644 --- a/src/writerai/_utils/_sync.py +++ b/src/writerai/_utils/_sync.py @@ -1,56 +1,62 @@ from __future__ import annotations +import sys +import asyncio import functools -from typing import TypeVar, Callable, Awaitable +import contextvars +from typing import Any, TypeVar, Callable, Awaitable from typing_extensions import ParamSpec -import anyio -import anyio.to_thread - -from ._reflection import function_has_argument - T_Retval = TypeVar("T_Retval") T_ParamSpec = ParamSpec("T_ParamSpec") -# copied from `asyncer`, https://github.com/tiangolo/asyncer -def asyncify( - function: Callable[T_ParamSpec, T_Retval], - *, - cancellable: bool = False, - limiter: anyio.CapacityLimiter | None = None, -) -> Callable[T_ParamSpec, Awaitable[T_Retval]]: +if sys.version_info >= (3, 9): + 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 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) + + +# inspired by `asyncer`, https://github.com/tiangolo/asyncer +def asyncify(function: Callable[T_ParamSpec, T_Retval]) -> Callable[T_ParamSpec, Awaitable[T_Retval]]: """ Take a blocking function and create an async one that receives the same - positional and keyword arguments, and that when called, calls the original function - in a worker thread using `anyio.to_thread.run_sync()`. Internally, - `asyncer.asyncify()` uses the same `anyio.to_thread.run_sync()`, but it supports - keyword arguments additional to positional arguments and it adds better support for - autocompletion and inline errors for the arguments of the function called and the - return value. - - If the `cancellable` option is enabled and the task waiting for its completion is - cancelled, the thread will still run its course but its return value (or any raised - exception) will be ignored. + 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. - Use it like this: + Usage: - ```Python - def do_work(arg1, arg2, kwarg1="", kwarg2="") -> str: - # Do work - return "Some result" + ```python + def blocking_func(arg1, arg2, kwarg1=None): + # blocking code + return result - result = await to_thread.asyncify(do_work)("spam", "ham", kwarg1="a", kwarg2="b") - print(result) + result = asyncify(blocking_function)(arg1, arg2, kwarg1=value1) ``` ## Arguments `function`: a blocking regular callable (e.g. a function) - `cancellable`: `True` to allow cancellation of the operation - `limiter`: capacity limiter to use to limit the total amount of threads running - (if omitted, the default limiter is used) ## Return @@ -60,22 +66,6 @@ def do_work(arg1, arg2, kwarg1="", kwarg2="") -> str: """ async def wrapper(*args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs) -> T_Retval: - partial_f = functools.partial(function, *args, **kwargs) - - # In `v4.1.0` anyio added the `abandon_on_cancel` argument and deprecated the old - # `cancellable` argument, so we need to use the new `abandon_on_cancel` to avoid - # surfacing deprecation warnings. - if function_has_argument(anyio.to_thread.run_sync, "abandon_on_cancel"): - return await anyio.to_thread.run_sync( - partial_f, - abandon_on_cancel=cancellable, - limiter=limiter, - ) - - return await anyio.to_thread.run_sync( - partial_f, - cancellable=cancellable, - limiter=limiter, - ) + return await to_thread(function, *args, **kwargs) return wrapper diff --git a/tests/test_client.py b/tests/test_client.py index 8d6c67a5..c1b16c8b 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -4,11 +4,14 @@ import gc import os +import sys import json import asyncio import inspect +import subprocess import tracemalloc from typing import Any, Union, cast +from textwrap import dedent from unittest import mock from typing_extensions import Literal @@ -1620,3 +1623,38 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" + + def test_get_platform(self) -> None: + # A previous implementation of asyncify could leave threads unterminated when + # used with nest_asyncio. + # + # Since nest_asyncio.apply() is global and cannot be un-applied, this + # test is run in a separate process to avoid affecting other tests. + test_code = dedent(""" + import asyncio + import nest_asyncio + import threading + + from writerai._utils import asyncify + from writerai._base_client import get_platform + + async def test_main() -> None: + result = await asyncify(get_platform)() + print(result) + for thread in threading.enumerate(): + print(thread.name) + + nest_asyncio.apply() + asyncio.run(test_main()) + """) + with subprocess.Popen( + [sys.executable, "-c", test_code], + text=True, + ) as process: + try: + process.wait(2) + if process.returncode: + raise AssertionError("calling get_platform using asyncify resulted in a non-zero exit code") + except subprocess.TimeoutExpired as e: + process.kill() + raise AssertionError("calling get_platform using asyncify resulted in a hung process") from e From 3cf5b6cded452b1f4946f599fa879feddde97c94 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 20 Nov 2024 09:38:40 +0000 Subject: [PATCH 123/399] feat(api): default timeout increase to 3 min (#119) --- README.md | 4 ++-- src/writerai/_constants.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 376c6bd8..10ccc602 100644 --- a/README.md +++ b/README.md @@ -246,7 +246,7 @@ client.with_options(max_retries=5).chat.chat( ### Timeouts -By default requests time out after 1 minute. You can configure this with a `timeout` option, +By default requests time out after 3 minutes. You can configure this with a `timeout` option, which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/#fine-tuning-the-configuration) object: ```python @@ -254,7 +254,7 @@ from writerai import Writer # Configure the default for all requests: client = Writer( - # 20 seconds (default is 1 minute) + # 20 seconds (default is 3 minutes) timeout=20.0, ) diff --git a/src/writerai/_constants.py b/src/writerai/_constants.py index a2ac3b6f..fb50e2c8 100644 --- a/src/writerai/_constants.py +++ b/src/writerai/_constants.py @@ -5,8 +5,8 @@ RAW_RESPONSE_HEADER = "X-Stainless-Raw-Response" OVERRIDE_CAST_TO_HEADER = "____stainless_override_cast_to" -# default timeout is 1 minute -DEFAULT_TIMEOUT = httpx.Timeout(timeout=60.0, connect=5.0) +# default timeout is 3 minutes +DEFAULT_TIMEOUT = httpx.Timeout(timeout=180.0, connect=5.0) DEFAULT_MAX_RETRIES = 2 DEFAULT_CONNECTION_LIMITS = httpx.Limits(max_connections=100, max_keepalive_connections=20) From 249ef93eca9dc8802551cfcc9ef868a91cabd3cf Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 20 Nov 2024 14:29:16 +0000 Subject: [PATCH 124/399] chore(internal): codegen related update (#120) --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 2a8f4ffd..3e9af1b3 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.3.0" + ".": "1.4.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 8abf6ffb..1becb558 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "1.3.0" +version = "1.4.0" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index b6bf1102..7f493856 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "1.3.0" # x-release-please-version +__version__ = "1.4.0" # x-release-please-version From b883471eb8a8a8a00403ad6f6c76ca2e68e13052 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 22 Nov 2024 23:12:06 +0000 Subject: [PATCH 125/399] chore(internal): fix compat model_dump method when warnings are passed (#121) --- src/writerai/_compat.py | 3 ++- tests/test_models.py | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/writerai/_compat.py b/src/writerai/_compat.py index 4794129c..df173f85 100644 --- a/src/writerai/_compat.py +++ b/src/writerai/_compat.py @@ -145,7 +145,8 @@ def model_dump( exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, - warnings=warnings, + # warnings are not supported in Pydantic v1 + warnings=warnings if PYDANTIC_V2 else True, ) return cast( "dict[str, Any]", diff --git a/tests/test_models.py b/tests/test_models.py index 6c746aee..2d00c359 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -561,6 +561,14 @@ class Model(BaseModel): m.model_dump(warnings=False) +def test_compat_method_no_error_for_warnings() -> None: + class Model(BaseModel): + foo: Optional[str] + + m = Model(foo="hello") + assert isinstance(model_dump(m, warnings=False), dict) + + def test_to_json() -> None: class Model(BaseModel): foo: Optional[str] = Field(alias="FOO", default=None) From 5f58fc0a489d39da3194a1bff50433038c41d42a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 22 Nov 2024 23:13:51 +0000 Subject: [PATCH 126/399] docs: add info log level to readme (#123) --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 10ccc602..ef552775 100644 --- a/README.md +++ b/README.md @@ -280,12 +280,14 @@ Note that requests that time out are [retried twice by default](#retries). We use the standard library [`logging`](https://docs.python.org/3/library/logging.html) module. -You can enable logging by setting the environment variable `WRITER_LOG` to `debug`. +You can enable logging by setting the environment variable `WRITER_LOG` to `info`. ```shell -$ export WRITER_LOG=debug +$ export WRITER_LOG=info ``` +Or to `debug` for more verbose logging. + ### How to tell whether `None` means `null` or missing In an API response, a field may be explicitly `null`, or missing entirely; in either case, its value is `None` in this library. You can differentiate the two cases with `.model_fields_set`: From 0881abb6cf40823812a1bdc14478fc7a151c6c95 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 12:22:32 +0000 Subject: [PATCH 127/399] chore: remove now unused `cached-property` dep (#124) --- pyproject.toml | 1 - src/writerai/_compat.py | 5 +---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1becb558..fb6d60d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,6 @@ dependencies = [ "anyio>=3.5.0, <5", "distro>=1.7.0, <2", "sniffio", - "cached-property; python_version < '3.8'", ] requires-python = ">= 3.8" classifiers = [ diff --git a/src/writerai/_compat.py b/src/writerai/_compat.py index df173f85..92d9ee61 100644 --- a/src/writerai/_compat.py +++ b/src/writerai/_compat.py @@ -214,9 +214,6 @@ def __set_name__(self, owner: type[Any], name: str) -> None: ... # __set__ is not defined at runtime, but @cached_property is designed to be settable def __set__(self, instance: object, value: _T) -> None: ... else: - try: - from functools import cached_property as cached_property - except ImportError: - from cached_property import cached_property as cached_property + from functools import cached_property as cached_property typed_cached_property = cached_property From 11fec0d6f2abfbdd71afc2315680db4f6a0d0796 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 27 Nov 2024 18:55:21 +0000 Subject: [PATCH 128/399] chore(internal): codegen related update (#125) --- .stats.yml | 2 +- README.md | 8 +- api.md | 33 ++- mypy.ini | 5 +- src/writerai/_streaming.py | 6 + src/writerai/resources/chat.py | 35 +-- src/writerai/resources/completions.py | 18 +- src/writerai/types/__init__.py | 22 +- src/writerai/types/chat.py | 222 ------------------ src/writerai/types/chat_chat_params.py | 125 +--------- src/writerai/types/chat_completion.py | 54 +++++ src/writerai/types/chat_completion_choice.py | 32 +++ src/writerai/types/chat_completion_chunk.py | 222 +----------------- src/writerai/types/chat_completion_message.py | 28 +++ src/writerai/types/chat_completion_usage.py | 27 +++ src/writerai/types/completion.py | 55 +---- ...{streaming_data.py => completion_chunk.py} | 4 +- src/writerai/types/question.py | 21 +- src/writerai/types/shared/__init__.py | 15 ++ src/writerai/types/shared/error_message.py | 15 ++ src/writerai/types/shared/error_object.py | 16 ++ .../types/shared/function_definition.py | 18 ++ src/writerai/types/shared/function_params.py | 8 + src/writerai/types/shared/graph_data.py | 27 +++ src/writerai/types/shared/logprobs.py | 14 ++ src/writerai/types/shared/logprobs_token.py | 25 ++ src/writerai/types/shared/source.py | 14 ++ src/writerai/types/shared/tool_call.py | 23 ++ .../types/shared/tool_call_streaming.py | 23 ++ .../types/shared/tool_choice_json_object.py | 11 + .../types/shared/tool_choice_string.py | 11 + src/writerai/types/shared/tool_param.py | 37 +++ src/writerai/types/shared_params/__init__.py | 10 + .../shared_params/function_definition.py | 19 ++ .../types/shared_params/function_params.py | 10 + .../types/shared_params/graph_data.py | 28 +++ src/writerai/types/shared_params/source.py | 15 ++ src/writerai/types/shared_params/tool_call.py | 23 ++ .../shared_params/tool_choice_json_object.py | 12 + .../types/shared_params/tool_choice_string.py | 11 + .../types/shared_params/tool_param.py | 38 +++ tests/api_resources/test_chat.py | 18 +- 42 files changed, 695 insertions(+), 665 deletions(-) delete mode 100644 src/writerai/types/chat.py create mode 100644 src/writerai/types/chat_completion.py create mode 100644 src/writerai/types/chat_completion_choice.py create mode 100644 src/writerai/types/chat_completion_message.py create mode 100644 src/writerai/types/chat_completion_usage.py rename src/writerai/types/{streaming_data.py => completion_chunk.py} (68%) create mode 100644 src/writerai/types/shared/__init__.py create mode 100644 src/writerai/types/shared/error_message.py create mode 100644 src/writerai/types/shared/error_object.py create mode 100644 src/writerai/types/shared/function_definition.py create mode 100644 src/writerai/types/shared/function_params.py create mode 100644 src/writerai/types/shared/graph_data.py create mode 100644 src/writerai/types/shared/logprobs.py create mode 100644 src/writerai/types/shared/logprobs_token.py create mode 100644 src/writerai/types/shared/source.py create mode 100644 src/writerai/types/shared/tool_call.py create mode 100644 src/writerai/types/shared/tool_call_streaming.py create mode 100644 src/writerai/types/shared/tool_choice_json_object.py create mode 100644 src/writerai/types/shared/tool_choice_string.py create mode 100644 src/writerai/types/shared/tool_param.py create mode 100644 src/writerai/types/shared_params/__init__.py create mode 100644 src/writerai/types/shared_params/function_definition.py create mode 100644 src/writerai/types/shared_params/function_params.py create mode 100644 src/writerai/types/shared_params/graph_data.py create mode 100644 src/writerai/types/shared_params/source.py create mode 100644 src/writerai/types/shared_params/tool_call.py create mode 100644 src/writerai/types/shared_params/tool_choice_json_object.py create mode 100644 src/writerai/types/shared_params/tool_choice_string.py create mode 100644 src/writerai/types/shared_params/tool_param.py diff --git a/.stats.yml b/.stats.yml index 40f1ba49..54ba5613 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-82683f2fd5f8778a27960ebabda40d6dc4640bdfb77ac4ec7f173b8bf8076d3c.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-6b363dd34169cab18f5ec3bcf6586aecd4799f79a80c90bf54e5a12f91d9e7c2.yml diff --git a/README.md b/README.md index ef552775..f54af779 100644 --- a/README.md +++ b/README.md @@ -31,11 +31,11 @@ client = Writer( api_key=os.environ.get("WRITER_API_KEY"), # This is the default and can be omitted ) -chat = client.chat.chat( +chat_completion = client.chat.chat( messages=[{"role": "user"}], model="palmyra-x-004", ) -print(chat.id) +print(chat_completion.id) ``` While you can provide an `api_key` keyword argument, @@ -58,11 +58,11 @@ client = AsyncWriter( async def main() -> None: - chat = await client.chat.chat( + chat_completion = await client.chat.chat( messages=[{"role": "user"}], model="palmyra-x-004", ) - print(chat.id) + print(chat_completion.id) asyncio.run(main()) diff --git a/api.md b/api.md index a86244a6..3c02faae 100644 --- a/api.md +++ b/api.md @@ -1,3 +1,23 @@ +# Shared Types + +```python +from writerai.types import ( + ErrorMessage, + ErrorObject, + FunctionDefinition, + FunctionParams, + GraphData, + Logprobs, + LogprobsToken, + Source, + ToolCall, + ToolCallStreaming, + ToolChoiceJsonObject, + ToolChoiceString, + ToolParam, +) +``` + # Applications Types: @@ -15,19 +35,26 @@ Methods: Types: ```python -from writerai.types import Chat, ChatCompletionChunk +from writerai.types import ( + ChatCompletion, + ChatCompletionChoice, + ChatCompletionChunk, + ChatCompletionMessage, + ChatCompletionParams, + ChatCompletionUsage, +) ``` Methods: -- client.chat.chat(\*\*params) -> Chat +- client.chat.chat(\*\*params) -> ChatCompletion # Completions Types: ```python -from writerai.types import Completion, StreamingData +from writerai.types import Completion, CompletionChunk, CompletionParams ``` Methods: diff --git a/mypy.ini b/mypy.ini index 1ece7ab6..29be567f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -5,7 +5,10 @@ show_error_codes = True # Exclude _files.py because mypy isn't smart enough to apply # the correct type narrowing and as this is an internal module # it's fine to just use Pyright. -exclude = ^(src/writerai/_files\.py|_dev/.*\.py)$ +# +# We also exclude our `tests` as mypy doesn't always infer +# types correctly and Pyright will still catch any type errors. +exclude = ^(src/writerai/_files\.py|_dev/.*\.py|tests/.*)$ strict_equality = True implicit_reexport = True diff --git a/src/writerai/_streaming.py b/src/writerai/_streaming.py index 794ea612..ca4711bc 100644 --- a/src/writerai/_streaming.py +++ b/src/writerai/_streaming.py @@ -55,6 +55,9 @@ def __stream__(self) -> Iterator[_T]: iterator = self._iter_events() for sse in iterator: + if sse.data.startswith("[DONE]"): + break + if sse.event is None: yield process_data(data=sse.json(), cast_to=cast_to, response=response) @@ -135,6 +138,9 @@ async def __stream__(self) -> AsyncIterator[_T]: iterator = self._iter_events() async for sse in iterator: + if sse.data.startswith("[DONE]"): + break + if sse.event is None: yield process_data(data=sse.json(), cast_to=cast_to, response=response) diff --git a/src/writerai/resources/chat.py b/src/writerai/resources/chat.py index 005aabad..7b7de07f 100644 --- a/src/writerai/resources/chat.py +++ b/src/writerai/resources/chat.py @@ -23,9 +23,10 @@ async_to_streamed_response_wrapper, ) from .._streaming import Stream, AsyncStream -from ..types.chat import Chat from .._base_client import make_request_options +from ..types.chat_completion import ChatCompletion from ..types.chat_completion_chunk import ChatCompletionChunk +from ..types.shared_params.tool_param import ToolParam __all__ = ["ChatResource", "AsyncChatResource"] @@ -64,7 +65,7 @@ def chat( stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, + tools: Iterable[ToolParam] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -72,7 +73,7 @@ def chat( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Chat: + ) -> ChatCompletion: """Generate a chat completion based on the provided messages. The response shown @@ -147,7 +148,7 @@ def chat( stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, + tools: Iterable[ToolParam] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -230,7 +231,7 @@ def chat( stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, + tools: Iterable[ToolParam] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -238,7 +239,7 @@ def chat( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Chat | Stream[ChatCompletionChunk]: + ) -> ChatCompletion | Stream[ChatCompletionChunk]: """Generate a chat completion based on the provided messages. The response shown @@ -313,7 +314,7 @@ def chat( stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, + tools: Iterable[ToolParam] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -321,7 +322,7 @@ def chat( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Chat | Stream[ChatCompletionChunk]: + ) -> ChatCompletion | Stream[ChatCompletionChunk]: return self._post( "/v1/chat", body=maybe_transform( @@ -344,7 +345,7 @@ def chat( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Chat, + cast_to=ChatCompletion, stream=stream or False, stream_cls=Stream[ChatCompletionChunk], ) @@ -384,7 +385,7 @@ async def chat( stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, + tools: Iterable[ToolParam] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -392,7 +393,7 @@ async def chat( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Chat: + ) -> ChatCompletion: """Generate a chat completion based on the provided messages. The response shown @@ -467,7 +468,7 @@ async def chat( stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, + tools: Iterable[ToolParam] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -550,7 +551,7 @@ async def chat( stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, + tools: Iterable[ToolParam] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -558,7 +559,7 @@ async def chat( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Chat | AsyncStream[ChatCompletionChunk]: + ) -> ChatCompletion | AsyncStream[ChatCompletionChunk]: """Generate a chat completion based on the provided messages. The response shown @@ -633,7 +634,7 @@ async def chat( stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, + tools: Iterable[ToolParam] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -641,7 +642,7 @@ async def chat( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Chat | AsyncStream[ChatCompletionChunk]: + ) -> ChatCompletion | AsyncStream[ChatCompletionChunk]: return await self._post( "/v1/chat", body=await async_maybe_transform( @@ -664,7 +665,7 @@ async def chat( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Chat, + cast_to=ChatCompletion, stream=stream or False, stream_cls=AsyncStream[ChatCompletionChunk], ) diff --git a/src/writerai/resources/completions.py b/src/writerai/resources/completions.py index 81162da0..b25c80be 100644 --- a/src/writerai/resources/completions.py +++ b/src/writerai/resources/completions.py @@ -25,7 +25,7 @@ from .._streaming import Stream, AsyncStream from .._base_client import make_request_options from ..types.completion import Completion -from ..types.streaming_data import StreamingData +from ..types.completion_chunk import CompletionChunk __all__ = ["CompletionsResource", "AsyncCompletionsResource"] @@ -130,7 +130,7 @@ def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Stream[StreamingData]: + ) -> Stream[CompletionChunk]: """ Text generation @@ -191,7 +191,7 @@ def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Completion | Stream[StreamingData]: + ) -> Completion | Stream[CompletionChunk]: """ Text generation @@ -252,7 +252,7 @@ def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Completion | Stream[StreamingData]: + ) -> Completion | Stream[CompletionChunk]: return self._post( "/v1/completions", body=maybe_transform( @@ -274,7 +274,7 @@ def create( ), cast_to=Completion, stream=stream or False, - stream_cls=Stream[StreamingData], + stream_cls=Stream[CompletionChunk], ) @@ -378,7 +378,7 @@ async def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AsyncStream[StreamingData]: + ) -> AsyncStream[CompletionChunk]: """ Text generation @@ -439,7 +439,7 @@ async def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Completion | AsyncStream[StreamingData]: + ) -> Completion | AsyncStream[CompletionChunk]: """ Text generation @@ -500,7 +500,7 @@ async def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Completion | AsyncStream[StreamingData]: + ) -> Completion | AsyncStream[CompletionChunk]: return await self._post( "/v1/completions", body=await async_maybe_transform( @@ -522,7 +522,7 @@ async def create( ), cast_to=Completion, stream=stream or False, - stream_cls=AsyncStream[StreamingData], + stream_cls=AsyncStream[CompletionChunk], ) diff --git a/src/writerai/types/__init__.py b/src/writerai/types/__init__.py index af6ad6c7..1679d6bf 100644 --- a/src/writerai/types/__init__.py +++ b/src/writerai/types/__init__.py @@ -2,13 +2,28 @@ from __future__ import annotations -from .chat import Chat as Chat from .file import File as File from .graph import Graph as Graph +from .shared import ( + Source as Source, + Logprobs as Logprobs, + ToolCall as ToolCall, + GraphData as GraphData, + ToolParam as ToolParam, + ErrorObject as ErrorObject, + ErrorMessage as ErrorMessage, + LogprobsToken as LogprobsToken, + FunctionParams as FunctionParams, + ToolChoiceString as ToolChoiceString, + ToolCallStreaming as ToolCallStreaming, + FunctionDefinition as FunctionDefinition, + ToolChoiceJsonObject as ToolChoiceJsonObject, +) from .question import Question as Question from .completion import Completion as Completion -from .streaming_data import StreamingData as StreamingData +from .chat_completion import ChatCompletion as ChatCompletion from .chat_chat_params import ChatChatParams as ChatChatParams +from .completion_chunk import CompletionChunk as CompletionChunk from .file_list_params import FileListParams as FileListParams from .file_retry_params import FileRetryParams as FileRetryParams from .graph_list_params import GraphListParams as GraphListParams @@ -19,11 +34,14 @@ from .model_list_response import ModelListResponse as ModelListResponse from .file_delete_response import FileDeleteResponse as FileDeleteResponse from .chat_completion_chunk import ChatCompletionChunk as ChatCompletionChunk +from .chat_completion_usage import ChatCompletionUsage as ChatCompletionUsage from .graph_create_response import GraphCreateResponse as GraphCreateResponse from .graph_delete_response import GraphDeleteResponse as GraphDeleteResponse from .graph_question_params import GraphQuestionParams as GraphQuestionParams from .graph_update_response import GraphUpdateResponse as GraphUpdateResponse from .tool_parse_pdf_params import ToolParsePdfParams as ToolParsePdfParams +from .chat_completion_choice import ChatCompletionChoice as ChatCompletionChoice +from .chat_completion_message import ChatCompletionMessage as ChatCompletionMessage from .question_response_chunk import QuestionResponseChunk as QuestionResponseChunk from .tool_parse_pdf_response import ToolParsePdfResponse as ToolParsePdfResponse from .completion_create_params import CompletionCreateParams as CompletionCreateParams diff --git a/src/writerai/types/chat.py b/src/writerai/types/chat.py deleted file mode 100644 index 48f7fd73..00000000 --- a/src/writerai/types/chat.py +++ /dev/null @@ -1,222 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List, Optional -from typing_extensions import Literal - -from .._models import BaseModel - -__all__ = [ - "Chat", - "Choice", - "ChoiceMessage", - "ChoiceMessageGraphData", - "ChoiceMessageGraphDataSource", - "ChoiceMessageGraphDataSubquery", - "ChoiceMessageGraphDataSubquerySource", - "ChoiceMessageToolCall", - "ChoiceMessageToolCallFunction", - "ChoiceLogprobs", - "ChoiceLogprobsContent", - "ChoiceLogprobsContentTopLogprob", - "ChoiceLogprobsRefusal", - "ChoiceLogprobsRefusalTopLogprob", - "Usage", - "UsageCompletionTokensDetails", - "UsagePromptTokenDetails", -] - - -class ChoiceMessageGraphDataSource(BaseModel): - file_id: str - """The unique identifier of the file.""" - - snippet: str - """A snippet of text from the source file.""" - - -class ChoiceMessageGraphDataSubquerySource(BaseModel): - file_id: str - """The unique identifier of the file.""" - - snippet: str - """A snippet of text from the source file.""" - - -class ChoiceMessageGraphDataSubquery(BaseModel): - answer: str - """The answer to the subquery.""" - - query: str - """The subquery that was asked.""" - - sources: List[ChoiceMessageGraphDataSubquerySource] - - -class ChoiceMessageGraphData(BaseModel): - sources: Optional[List[ChoiceMessageGraphDataSource]] = None - - status: Optional[Literal["processing", "finished"]] = None - - subqueries: Optional[List[ChoiceMessageGraphDataSubquery]] = None - - -class ChoiceMessageToolCallFunction(BaseModel): - arguments: str - - name: str - - -class ChoiceMessageToolCall(BaseModel): - id: str - - function: ChoiceMessageToolCallFunction - - type: str - - index: Optional[int] = None - - -class ChoiceMessage(BaseModel): - content: str - """The text content produced by the model. - - This field contains the actual output generated, reflecting the model's response - to the input query or command. - """ - - refusal: Optional[str] = None - - role: Literal["assistant"] - """Specifies the role associated with the content.""" - - graph_data: Optional[ChoiceMessageGraphData] = None - - tool_calls: Optional[List[ChoiceMessageToolCall]] = None - - -class ChoiceLogprobsContentTopLogprob(BaseModel): - token: str - - logprob: float - - bytes: Optional[List[int]] = None - - -class ChoiceLogprobsContent(BaseModel): - token: str - - logprob: float - - top_logprobs: List[ChoiceLogprobsContentTopLogprob] - - bytes: Optional[List[int]] = None - - -class ChoiceLogprobsRefusalTopLogprob(BaseModel): - token: str - - logprob: float - - bytes: Optional[List[int]] = None - - -class ChoiceLogprobsRefusal(BaseModel): - token: str - - logprob: float - - top_logprobs: List[ChoiceLogprobsRefusalTopLogprob] - - bytes: Optional[List[int]] = None - - -class ChoiceLogprobs(BaseModel): - content: Optional[List[ChoiceLogprobsContent]] = None - - refusal: Optional[List[ChoiceLogprobsRefusal]] = None - - -class Choice(BaseModel): - finish_reason: Literal["stop", "length", "content_filter", "tool_calls"] - """Describes the condition under which the model ceased generating content. - - Common reasons include 'length' (reached the maximum output size), 'stop' - (encountered a stop sequence), 'content_filter' (harmful content filtered out), - or 'tool_calls' (encountered tool calls). - """ - - index: int - """The index of the choice in the list of completions generated by the model.""" - - message: ChoiceMessage - """The chat completion message from the model. - - Note: this field is deprecated for streaming. Use `delta` instead. - """ - - logprobs: Optional[ChoiceLogprobs] = None - """Log probability information for the choice.""" - - -class UsageCompletionTokensDetails(BaseModel): - reasoning_tokens: int - - -class UsagePromptTokenDetails(BaseModel): - cached_tokens: int - - -class Usage(BaseModel): - completion_tokens: int - - prompt_tokens: int - - total_tokens: int - - completion_tokens_details: Optional[UsageCompletionTokensDetails] = None - - prompt_token_details: Optional[UsagePromptTokenDetails] = None - - -class Chat(BaseModel): - id: str - """A globally unique identifier (UUID) for the response generated by the API. - - This ID can be used to reference the specific operation or transaction within - the system for tracking or debugging purposes. - """ - - choices: List[Choice] - """ - An array of objects representing the different outcomes or results produced by - the model based on the input provided. - """ - - created: int - """The Unix timestamp (in seconds) when the response was created. - - This timestamp can be used to verify the timing of the response relative to - other events or operations. - """ - - model: str - """Identifies the specific model used to generate the response.""" - - object: Literal["chat.completion"] - """ - The type of object returned, which is always `chat.completion` for chat - responses. - """ - - service_tier: Optional[str] = None - """The service tier used for processing the request.""" - - system_fingerprint: Optional[str] = None - """A string representing the backend configuration that the model runs with.""" - - usage: Optional[Usage] = None - """Usage information for the chat completion response. - - Please note that at this time Knowledge Graph tool usage is not included in this - object. - """ diff --git a/src/writerai/types/chat_chat_params.py b/src/writerai/types/chat_chat_params.py index 4bf0acd7..9216e4cf 100644 --- a/src/writerai/types/chat_chat_params.py +++ b/src/writerai/types/chat_chat_params.py @@ -2,27 +2,20 @@ from __future__ import annotations -from typing import Dict, List, Union, Iterable, Optional +from typing import List, Union, Iterable, Optional from typing_extensions import Literal, Required, TypeAlias, TypedDict +from .shared_params.tool_call import ToolCall +from .shared_params.graph_data import GraphData +from .shared_params.tool_param import ToolParam +from .shared_params.tool_choice_string import ToolChoiceString +from .shared_params.tool_choice_json_object import ToolChoiceJsonObject + __all__ = [ "ChatChatParamsBase", "Message", - "MessageGraphData", - "MessageGraphDataSource", - "MessageGraphDataSubquery", - "MessageGraphDataSubquerySource", - "MessageToolCall", - "MessageToolCallFunction", "StreamOptions", "ToolChoice", - "ToolChoiceStringToolChoice", - "ToolChoiceJsonObjectToolChoice", - "Tool", - "ToolFunctionTool", - "ToolFunctionToolFunction", - "ToolGraphTool", - "ToolGraphToolFunction", "ChatChatParamsNonStreaming", "ChatChatParamsStreaming", ] @@ -82,7 +75,7 @@ class ChatChatParamsBase(TypedDict, total=False): pass a specific previously defined function. """ - tools: Iterable[Tool] + tools: Iterable[ToolParam] """ An array of tools described to the model using JSON schema that the model can use to generate responses. Passing graph IDs will automatically use the @@ -98,62 +91,12 @@ class ChatChatParamsBase(TypedDict, total=False): """ -class MessageGraphDataSource(TypedDict, total=False): - file_id: Required[str] - """The unique identifier of the file.""" - - snippet: Required[str] - """A snippet of text from the source file.""" - - -class MessageGraphDataSubquerySource(TypedDict, total=False): - file_id: Required[str] - """The unique identifier of the file.""" - - snippet: Required[str] - """A snippet of text from the source file.""" - - -class MessageGraphDataSubquery(TypedDict, total=False): - answer: Required[str] - """The answer to the subquery.""" - - query: Required[str] - """The subquery that was asked.""" - - sources: Required[Iterable[MessageGraphDataSubquerySource]] - - -class MessageGraphData(TypedDict, total=False): - sources: Iterable[MessageGraphDataSource] - - status: Literal["processing", "finished"] - - subqueries: Iterable[MessageGraphDataSubquery] - - -class MessageToolCallFunction(TypedDict, total=False): - arguments: Required[str] - - name: Required[str] - - -class MessageToolCall(TypedDict, total=False): - id: Required[str] - - function: Required[MessageToolCallFunction] - - type: Required[str] - - index: int - - class Message(TypedDict, total=False): role: Required[Literal["user", "assistant", "system", "tool"]] content: Optional[str] - graph_data: Optional[MessageGraphData] + graph_data: Optional[GraphData] name: Optional[str] @@ -161,7 +104,7 @@ class Message(TypedDict, total=False): tool_call_id: Optional[str] - tool_calls: Optional[Iterable[MessageToolCall]] + tool_calls: Optional[Iterable[ToolCall]] class StreamOptions(TypedDict, total=False): @@ -169,53 +112,7 @@ class StreamOptions(TypedDict, total=False): """Indicate whether to include usage information.""" -class ToolChoiceStringToolChoice(TypedDict, total=False): - value: Required[Literal["none", "auto", "required"]] - - -class ToolChoiceJsonObjectToolChoice(TypedDict, total=False): - value: Required[Dict[str, object]] - - -ToolChoice: TypeAlias = Union[ToolChoiceStringToolChoice, ToolChoiceJsonObjectToolChoice] - - -class ToolFunctionToolFunction(TypedDict, total=False): - name: Required[str] - """Name of the function""" - - description: str - """Description of the function""" - - parameters: Dict[str, object] - - -class ToolFunctionTool(TypedDict, total=False): - function: Required[ToolFunctionToolFunction] - - type: Required[Literal["function"]] - """The type of tool.""" - - -class ToolGraphToolFunction(TypedDict, total=False): - graph_ids: Required[List[str]] - """An array of graph IDs to be used in the tool.""" - - subqueries: Required[bool] - """Boolean to indicate whether to include subqueries in the response.""" - - description: str - """A description of the graph content.""" - - -class ToolGraphTool(TypedDict, total=False): - function: Required[ToolGraphToolFunction] - - type: Required[Literal["graph"]] - """The type of tool.""" - - -Tool: TypeAlias = Union[ToolFunctionTool, ToolGraphTool] +ToolChoice: TypeAlias = Union[ToolChoiceString, ToolChoiceJsonObject] class ChatChatParamsNonStreaming(ChatChatParamsBase, total=False): diff --git a/src/writerai/types/chat_completion.py b/src/writerai/types/chat_completion.py new file mode 100644 index 00000000..a7521fff --- /dev/null +++ b/src/writerai/types/chat_completion.py @@ -0,0 +1,54 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import Literal + +from .._models import BaseModel +from .chat_completion_usage import ChatCompletionUsage +from .chat_completion_choice import ChatCompletionChoice + +__all__ = ["ChatCompletion"] + + +class ChatCompletion(BaseModel): + id: str + """A globally unique identifier (UUID) for the response generated by the API. + + This ID can be used to reference the specific operation or transaction within + the system for tracking or debugging purposes. + """ + + choices: List[ChatCompletionChoice] + """ + An array of objects representing the different outcomes or results produced by + the model based on the input provided. + """ + + created: int + """The Unix timestamp (in seconds) when the response was created. + + This timestamp can be used to verify the timing of the response relative to + other events or operations. + """ + + model: str + """Identifies the specific model used to generate the response.""" + + object: Literal["chat.completion"] + """ + The type of object returned, which is always `chat.completion` for chat + responses. + """ + + service_tier: Optional[str] = None + """The service tier used for processing the request.""" + + system_fingerprint: Optional[str] = None + """A string representing the backend configuration that the model runs with.""" + + usage: Optional[ChatCompletionUsage] = None + """Usage information for the chat completion response. + + Please note that at this time Knowledge Graph tool usage is not included in this + object. + """ diff --git a/src/writerai/types/chat_completion_choice.py b/src/writerai/types/chat_completion_choice.py new file mode 100644 index 00000000..f96b5bee --- /dev/null +++ b/src/writerai/types/chat_completion_choice.py @@ -0,0 +1,32 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from .._models import BaseModel +from .shared.logprobs import Logprobs +from .chat_completion_message import ChatCompletionMessage + +__all__ = ["ChatCompletionChoice"] + + +class ChatCompletionChoice(BaseModel): + finish_reason: Literal["stop", "length", "content_filter", "tool_calls"] + """Describes the condition under which the model ceased generating content. + + Common reasons include 'length' (reached the maximum output size), 'stop' + (encountered a stop sequence), 'content_filter' (harmful content filtered out), + or 'tool_calls' (encountered tool calls). + """ + + index: int + """The index of the choice in the list of completions generated by the model.""" + + message: ChatCompletionMessage + """The chat completion message from the model. + + Note: this field is deprecated for streaming. Use `delta` instead. + """ + + logprobs: Optional[Logprobs] = None + """Log probability information for the choice.""" diff --git a/src/writerai/types/chat_completion_chunk.py b/src/writerai/types/chat_completion_chunk.py index 0a6fe71a..681cc663 100644 --- a/src/writerai/types/chat_completion_chunk.py +++ b/src/writerai/types/chat_completion_chunk.py @@ -4,83 +4,13 @@ from typing_extensions import Literal from .._models import BaseModel +from .shared.logprobs import Logprobs +from .shared.graph_data import GraphData +from .chat_completion_usage import ChatCompletionUsage +from .chat_completion_message import ChatCompletionMessage +from .shared.tool_call_streaming import ToolCallStreaming -__all__ = [ - "ChatCompletionChunk", - "Choice", - "ChoiceDelta", - "ChoiceDeltaGraphData", - "ChoiceDeltaGraphDataSource", - "ChoiceDeltaGraphDataSubquery", - "ChoiceDeltaGraphDataSubquerySource", - "ChoiceDeltaToolCall", - "ChoiceDeltaToolCallFunction", - "ChoiceLogprobs", - "ChoiceLogprobsContent", - "ChoiceLogprobsContentTopLogprob", - "ChoiceLogprobsRefusal", - "ChoiceLogprobsRefusalTopLogprob", - "ChoiceMessage", - "ChoiceMessageGraphData", - "ChoiceMessageGraphDataSource", - "ChoiceMessageGraphDataSubquery", - "ChoiceMessageGraphDataSubquerySource", - "ChoiceMessageToolCall", - "ChoiceMessageToolCallFunction", - "Usage", - "UsageCompletionTokensDetails", - "UsagePromptTokenDetails", -] - - -class ChoiceDeltaGraphDataSource(BaseModel): - file_id: str - """The unique identifier of the file.""" - - snippet: str - """A snippet of text from the source file.""" - - -class ChoiceDeltaGraphDataSubquerySource(BaseModel): - file_id: str - """The unique identifier of the file.""" - - snippet: str - """A snippet of text from the source file.""" - - -class ChoiceDeltaGraphDataSubquery(BaseModel): - answer: str - """The answer to the subquery.""" - - query: str - """The subquery that was asked.""" - - sources: List[ChoiceDeltaGraphDataSubquerySource] - - -class ChoiceDeltaGraphData(BaseModel): - sources: Optional[List[ChoiceDeltaGraphDataSource]] = None - - status: Optional[Literal["processing", "finished"]] = None - - subqueries: Optional[List[ChoiceDeltaGraphDataSubquery]] = None - - -class ChoiceDeltaToolCallFunction(BaseModel): - arguments: str - - name: str - - -class ChoiceDeltaToolCall(BaseModel): - index: int - - id: Optional[str] = None - - function: Optional[ChoiceDeltaToolCallFunction] = None - - type: Optional[str] = None +__all__ = ["ChatCompletionChunk", "Choice", "ChoiceDelta"] class ChoiceDelta(BaseModel): @@ -91,7 +21,7 @@ class ChoiceDelta(BaseModel): to the input query or command. """ - graph_data: Optional[ChoiceDeltaGraphData] = None + graph_data: Optional[GraphData] = None refusal: Optional[str] = None @@ -102,117 +32,7 @@ class ChoiceDelta(BaseModel): output within the interaction flow. """ - tool_calls: Optional[List[ChoiceDeltaToolCall]] = None - - -class ChoiceLogprobsContentTopLogprob(BaseModel): - token: str - - logprob: float - - bytes: Optional[List[int]] = None - - -class ChoiceLogprobsContent(BaseModel): - token: str - - logprob: float - - top_logprobs: List[ChoiceLogprobsContentTopLogprob] - - bytes: Optional[List[int]] = None - - -class ChoiceLogprobsRefusalTopLogprob(BaseModel): - token: str - - logprob: float - - bytes: Optional[List[int]] = None - - -class ChoiceLogprobsRefusal(BaseModel): - token: str - - logprob: float - - top_logprobs: List[ChoiceLogprobsRefusalTopLogprob] - - bytes: Optional[List[int]] = None - - -class ChoiceLogprobs(BaseModel): - content: Optional[List[ChoiceLogprobsContent]] = None - - refusal: Optional[List[ChoiceLogprobsRefusal]] = None - - -class ChoiceMessageGraphDataSource(BaseModel): - file_id: str - """The unique identifier of the file.""" - - snippet: str - """A snippet of text from the source file.""" - - -class ChoiceMessageGraphDataSubquerySource(BaseModel): - file_id: str - """The unique identifier of the file.""" - - snippet: str - """A snippet of text from the source file.""" - - -class ChoiceMessageGraphDataSubquery(BaseModel): - answer: str - """The answer to the subquery.""" - - query: str - """The subquery that was asked.""" - - sources: List[ChoiceMessageGraphDataSubquerySource] - - -class ChoiceMessageGraphData(BaseModel): - sources: Optional[List[ChoiceMessageGraphDataSource]] = None - - status: Optional[Literal["processing", "finished"]] = None - - subqueries: Optional[List[ChoiceMessageGraphDataSubquery]] = None - - -class ChoiceMessageToolCallFunction(BaseModel): - arguments: str - - name: str - - -class ChoiceMessageToolCall(BaseModel): - id: str - - function: ChoiceMessageToolCallFunction - - type: str - - index: Optional[int] = None - - -class ChoiceMessage(BaseModel): - content: str - """The text content produced by the model. - - This field contains the actual output generated, reflecting the model's response - to the input query or command. - """ - - refusal: Optional[str] = None - - role: Literal["assistant"] - """Specifies the role associated with the content.""" - - graph_data: Optional[ChoiceMessageGraphData] = None - - tool_calls: Optional[List[ChoiceMessageToolCall]] = None + tool_calls: Optional[List[ToolCallStreaming]] = None class Choice(BaseModel): @@ -230,36 +50,16 @@ class Choice(BaseModel): index: int """The index of the choice in the list of completions generated by the model.""" - logprobs: Optional[ChoiceLogprobs] = None + logprobs: Optional[Logprobs] = None """Log probability information for the choice.""" - message: Optional[ChoiceMessage] = None + message: Optional[ChatCompletionMessage] = None """The chat completion message from the model. Note: this field is deprecated for streaming. Use `delta` instead. """ -class UsageCompletionTokensDetails(BaseModel): - reasoning_tokens: int - - -class UsagePromptTokenDetails(BaseModel): - cached_tokens: int - - -class Usage(BaseModel): - completion_tokens: int - - prompt_tokens: int - - total_tokens: int - - completion_tokens_details: Optional[UsageCompletionTokensDetails] = None - - prompt_token_details: Optional[UsagePromptTokenDetails] = None - - class ChatCompletionChunk(BaseModel): id: str """A globally unique identifier (UUID) for the response generated by the API. @@ -294,7 +94,7 @@ class ChatCompletionChunk(BaseModel): system_fingerprint: Optional[str] = None - usage: Optional[Usage] = None + usage: Optional[ChatCompletionUsage] = None """Usage information for the chat completion response. Please note that at this time Knowledge Graph tool usage is not included in this diff --git a/src/writerai/types/chat_completion_message.py b/src/writerai/types/chat_completion_message.py new file mode 100644 index 00000000..4187ea36 --- /dev/null +++ b/src/writerai/types/chat_completion_message.py @@ -0,0 +1,28 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import Literal + +from .._models import BaseModel +from .shared.tool_call import ToolCall +from .shared.graph_data import GraphData + +__all__ = ["ChatCompletionMessage"] + + +class ChatCompletionMessage(BaseModel): + content: str + """The text content produced by the model. + + This field contains the actual output generated, reflecting the model's response + to the input query or command. + """ + + refusal: Optional[str] = None + + role: Literal["assistant"] + """Specifies the role associated with the content.""" + + graph_data: Optional[GraphData] = None + + tool_calls: Optional[List[ToolCall]] = None diff --git a/src/writerai/types/chat_completion_usage.py b/src/writerai/types/chat_completion_usage.py new file mode 100644 index 00000000..6fc89018 --- /dev/null +++ b/src/writerai/types/chat_completion_usage.py @@ -0,0 +1,27 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel + +__all__ = ["ChatCompletionUsage", "CompletionTokensDetails", "PromptTokenDetails"] + + +class CompletionTokensDetails(BaseModel): + reasoning_tokens: int + + +class PromptTokenDetails(BaseModel): + cached_tokens: int + + +class ChatCompletionUsage(BaseModel): + completion_tokens: int + + prompt_tokens: int + + total_tokens: int + + completion_tokens_details: Optional[CompletionTokensDetails] = None + + prompt_token_details: Optional[PromptTokenDetails] = None diff --git a/src/writerai/types/completion.py b/src/writerai/types/completion.py index 5f4e82ae..01fe40e5 100644 --- a/src/writerai/types/completion.py +++ b/src/writerai/types/completion.py @@ -3,58 +3,9 @@ from typing import List, Optional from .._models import BaseModel +from .shared.logprobs import Logprobs -__all__ = [ - "Completion", - "Choice", - "ChoiceLogProbs", - "ChoiceLogProbsContent", - "ChoiceLogProbsContentTopLogprob", - "ChoiceLogProbsRefusal", - "ChoiceLogProbsRefusalTopLogprob", -] - - -class ChoiceLogProbsContentTopLogprob(BaseModel): - token: str - - logprob: float - - bytes: Optional[List[int]] = None - - -class ChoiceLogProbsContent(BaseModel): - token: str - - logprob: float - - top_logprobs: List[ChoiceLogProbsContentTopLogprob] - - bytes: Optional[List[int]] = None - - -class ChoiceLogProbsRefusalTopLogprob(BaseModel): - token: str - - logprob: float - - bytes: Optional[List[int]] = None - - -class ChoiceLogProbsRefusal(BaseModel): - token: str - - logprob: float - - top_logprobs: List[ChoiceLogProbsRefusalTopLogprob] - - bytes: Optional[List[int]] = None - - -class ChoiceLogProbs(BaseModel): - content: Optional[List[ChoiceLogProbsContent]] = None - - refusal: Optional[List[ChoiceLogProbsRefusal]] = None +__all__ = ["Completion", "Choice"] class Choice(BaseModel): @@ -64,7 +15,7 @@ class Choice(BaseModel): response. """ - log_probs: Optional[ChoiceLogProbs] = None + log_probs: Optional[Logprobs] = None class Completion(BaseModel): diff --git a/src/writerai/types/streaming_data.py b/src/writerai/types/completion_chunk.py similarity index 68% rename from src/writerai/types/streaming_data.py rename to src/writerai/types/completion_chunk.py index 8c88b456..aec58bd3 100644 --- a/src/writerai/types/streaming_data.py +++ b/src/writerai/types/completion_chunk.py @@ -3,8 +3,8 @@ from .._models import BaseModel -__all__ = ["StreamingData"] +__all__ = ["CompletionChunk"] -class StreamingData(BaseModel): +class CompletionChunk(BaseModel): value: str diff --git a/src/writerai/types/question.py b/src/writerai/types/question.py index 24473a47..4776dfbd 100644 --- a/src/writerai/types/question.py +++ b/src/writerai/types/question.py @@ -3,24 +3,9 @@ from typing import List, Optional from .._models import BaseModel +from .shared.source import Source -__all__ = ["Question", "Source", "Subquery", "SubquerySource"] - - -class Source(BaseModel): - file_id: str - """The unique identifier of the file.""" - - snippet: str - """A snippet of text from the source file.""" - - -class SubquerySource(BaseModel): - file_id: str - """The unique identifier of the file.""" - - snippet: str - """A snippet of text from the source file.""" +__all__ = ["Question", "Subquery"] class Subquery(BaseModel): @@ -30,7 +15,7 @@ class Subquery(BaseModel): query: str """The subquery that was asked.""" - sources: List[SubquerySource] + sources: List[Source] class Question(BaseModel): diff --git a/src/writerai/types/shared/__init__.py b/src/writerai/types/shared/__init__.py new file mode 100644 index 00000000..d62055d0 --- /dev/null +++ b/src/writerai/types/shared/__init__.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .source import Source as Source +from .logprobs import Logprobs as Logprobs +from .tool_call import ToolCall as ToolCall +from .graph_data import GraphData as GraphData +from .tool_param import ToolParam as ToolParam +from .error_object import ErrorObject as ErrorObject +from .error_message import ErrorMessage as ErrorMessage +from .logprobs_token import LogprobsToken as LogprobsToken +from .function_params import FunctionParams as FunctionParams +from .tool_choice_string import ToolChoiceString as ToolChoiceString +from .function_definition import FunctionDefinition as FunctionDefinition +from .tool_call_streaming import ToolCallStreaming as ToolCallStreaming +from .tool_choice_json_object import ToolChoiceJsonObject as ToolChoiceJsonObject diff --git a/src/writerai/types/shared/error_message.py b/src/writerai/types/shared/error_message.py new file mode 100644 index 00000000..2b5b1599 --- /dev/null +++ b/src/writerai/types/shared/error_message.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict + +from ..._models import BaseModel + +__all__ = ["ErrorMessage"] + + +class ErrorMessage(BaseModel): + description: str + + extras: Dict[str, object] + + key: str diff --git a/src/writerai/types/shared/error_object.py b/src/writerai/types/shared/error_object.py new file mode 100644 index 00000000..e98f7a5e --- /dev/null +++ b/src/writerai/types/shared/error_object.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, List + +from ..._models import BaseModel +from .error_message import ErrorMessage + +__all__ = ["ErrorObject"] + + +class ErrorObject(BaseModel): + errors: List[ErrorMessage] + + extras: Dict[str, object] + + tpe: str diff --git a/src/writerai/types/shared/function_definition.py b/src/writerai/types/shared/function_definition.py new file mode 100644 index 00000000..c4f1e678 --- /dev/null +++ b/src/writerai/types/shared/function_definition.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel +from .function_params import FunctionParams + +__all__ = ["FunctionDefinition"] + + +class FunctionDefinition(BaseModel): + name: str + """Name of the function""" + + description: Optional[str] = None + """Description of the function""" + + parameters: Optional[FunctionParams] = None diff --git a/src/writerai/types/shared/function_params.py b/src/writerai/types/shared/function_params.py new file mode 100644 index 00000000..cb00a506 --- /dev/null +++ b/src/writerai/types/shared/function_params.py @@ -0,0 +1,8 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict +from typing_extensions import TypeAlias + +__all__ = ["FunctionParams"] + +FunctionParams: TypeAlias = Dict[str, object] diff --git a/src/writerai/types/shared/graph_data.py b/src/writerai/types/shared/graph_data.py new file mode 100644 index 00000000..b0f21bfb --- /dev/null +++ b/src/writerai/types/shared/graph_data.py @@ -0,0 +1,27 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import Literal + +from .source import Source +from ..._models import BaseModel + +__all__ = ["GraphData", "Subquery"] + + +class Subquery(BaseModel): + answer: str + """The answer to the subquery.""" + + query: str + """The subquery that was asked.""" + + sources: List[Source] + + +class GraphData(BaseModel): + sources: Optional[List[Source]] = None + + status: Optional[Literal["processing", "finished"]] = None + + subqueries: Optional[List[Subquery]] = None diff --git a/src/writerai/types/shared/logprobs.py b/src/writerai/types/shared/logprobs.py new file mode 100644 index 00000000..33dbf7ed --- /dev/null +++ b/src/writerai/types/shared/logprobs.py @@ -0,0 +1,14 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from ..._models import BaseModel +from .logprobs_token import LogprobsToken + +__all__ = ["Logprobs"] + + +class Logprobs(BaseModel): + content: Optional[List[LogprobsToken]] = None + + refusal: Optional[List[LogprobsToken]] = None diff --git a/src/writerai/types/shared/logprobs_token.py b/src/writerai/types/shared/logprobs_token.py new file mode 100644 index 00000000..40c58d67 --- /dev/null +++ b/src/writerai/types/shared/logprobs_token.py @@ -0,0 +1,25 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from ..._models import BaseModel + +__all__ = ["LogprobsToken", "TopLogprob"] + + +class TopLogprob(BaseModel): + token: str + + logprob: float + + bytes: Optional[List[int]] = None + + +class LogprobsToken(BaseModel): + token: str + + logprob: float + + top_logprobs: List[TopLogprob] + + bytes: Optional[List[int]] = None diff --git a/src/writerai/types/shared/source.py b/src/writerai/types/shared/source.py new file mode 100644 index 00000000..f737aa17 --- /dev/null +++ b/src/writerai/types/shared/source.py @@ -0,0 +1,14 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + + +from ..._models import BaseModel + +__all__ = ["Source"] + + +class Source(BaseModel): + file_id: str + """The unique identifier of the file.""" + + snippet: str + """A snippet of text from the source file.""" diff --git a/src/writerai/types/shared/tool_call.py b/src/writerai/types/shared/tool_call.py new file mode 100644 index 00000000..d21d0cc1 --- /dev/null +++ b/src/writerai/types/shared/tool_call.py @@ -0,0 +1,23 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel + +__all__ = ["ToolCall", "Function"] + + +class Function(BaseModel): + arguments: str + + name: Optional[str] = None + + +class ToolCall(BaseModel): + id: str + + function: Function + + type: str + + index: Optional[int] = None diff --git a/src/writerai/types/shared/tool_call_streaming.py b/src/writerai/types/shared/tool_call_streaming.py new file mode 100644 index 00000000..350001f2 --- /dev/null +++ b/src/writerai/types/shared/tool_call_streaming.py @@ -0,0 +1,23 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel + +__all__ = ["ToolCallStreaming", "Function"] + + +class Function(BaseModel): + arguments: str + + name: Optional[str] = None + + +class ToolCallStreaming(BaseModel): + index: int + + id: Optional[str] = None + + function: Optional[Function] = None + + type: Optional[str] = None diff --git a/src/writerai/types/shared/tool_choice_json_object.py b/src/writerai/types/shared/tool_choice_json_object.py new file mode 100644 index 00000000..bc12acf5 --- /dev/null +++ b/src/writerai/types/shared/tool_choice_json_object.py @@ -0,0 +1,11 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict + +from ..._models import BaseModel + +__all__ = ["ToolChoiceJsonObject"] + + +class ToolChoiceJsonObject(BaseModel): + value: Dict[str, object] diff --git a/src/writerai/types/shared/tool_choice_string.py b/src/writerai/types/shared/tool_choice_string.py new file mode 100644 index 00000000..653664da --- /dev/null +++ b/src/writerai/types/shared/tool_choice_string.py @@ -0,0 +1,11 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["ToolChoiceString"] + + +class ToolChoiceString(BaseModel): + value: Literal["none", "auto", "required"] diff --git a/src/writerai/types/shared/tool_param.py b/src/writerai/types/shared/tool_param.py new file mode 100644 index 00000000..93b5ac25 --- /dev/null +++ b/src/writerai/types/shared/tool_param.py @@ -0,0 +1,37 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Union, Optional +from typing_extensions import Literal, TypeAlias + +from ..._models import BaseModel +from .function_definition import FunctionDefinition + +__all__ = ["ToolParam", "FunctionTool", "GraphTool", "GraphToolFunction"] + + +class FunctionTool(BaseModel): + function: FunctionDefinition + + type: Literal["function"] + """The type of tool.""" + + +class GraphToolFunction(BaseModel): + graph_ids: List[str] + """An array of graph IDs to be used in the tool.""" + + subqueries: bool + """Boolean to indicate whether to include subqueries in the response.""" + + description: Optional[str] = None + """A description of the graph content.""" + + +class GraphTool(BaseModel): + function: GraphToolFunction + + type: Literal["graph"] + """The type of tool.""" + + +ToolParam: TypeAlias = Union[FunctionTool, GraphTool] diff --git a/src/writerai/types/shared_params/__init__.py b/src/writerai/types/shared_params/__init__.py new file mode 100644 index 00000000..1bd920cf --- /dev/null +++ b/src/writerai/types/shared_params/__init__.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .source import Source as Source +from .tool_call import ToolCall as ToolCall +from .graph_data import GraphData as GraphData +from .tool_param import ToolParam as ToolParam +from .function_params import FunctionParams as FunctionParams +from .tool_choice_string import ToolChoiceString as ToolChoiceString +from .function_definition import FunctionDefinition as FunctionDefinition +from .tool_choice_json_object import ToolChoiceJsonObject as ToolChoiceJsonObject diff --git a/src/writerai/types/shared_params/function_definition.py b/src/writerai/types/shared_params/function_definition.py new file mode 100644 index 00000000..4043fa70 --- /dev/null +++ b/src/writerai/types/shared_params/function_definition.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +from .function_params import FunctionParams + +__all__ = ["FunctionDefinition"] + + +class FunctionDefinition(TypedDict, total=False): + name: Required[str] + """Name of the function""" + + description: str + """Description of the function""" + + parameters: FunctionParams diff --git a/src/writerai/types/shared_params/function_params.py b/src/writerai/types/shared_params/function_params.py new file mode 100644 index 00000000..dd1f1c2a --- /dev/null +++ b/src/writerai/types/shared_params/function_params.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict +from typing_extensions import TypeAlias + +__all__ = ["FunctionParams"] + +FunctionParams: TypeAlias = Dict[str, object] diff --git a/src/writerai/types/shared_params/graph_data.py b/src/writerai/types/shared_params/graph_data.py new file mode 100644 index 00000000..d89894c0 --- /dev/null +++ b/src/writerai/types/shared_params/graph_data.py @@ -0,0 +1,28 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Iterable +from typing_extensions import Literal, Required, TypedDict + +from .source import Source + +__all__ = ["GraphData", "Subquery"] + + +class Subquery(TypedDict, total=False): + answer: Required[str] + """The answer to the subquery.""" + + query: Required[str] + """The subquery that was asked.""" + + sources: Required[Iterable[Source]] + + +class GraphData(TypedDict, total=False): + sources: Iterable[Source] + + status: Literal["processing", "finished"] + + subqueries: Iterable[Subquery] diff --git a/src/writerai/types/shared_params/source.py b/src/writerai/types/shared_params/source.py new file mode 100644 index 00000000..54b70059 --- /dev/null +++ b/src/writerai/types/shared_params/source.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["Source"] + + +class Source(TypedDict, total=False): + file_id: Required[str] + """The unique identifier of the file.""" + + snippet: Required[str] + """A snippet of text from the source file.""" diff --git a/src/writerai/types/shared_params/tool_call.py b/src/writerai/types/shared_params/tool_call.py new file mode 100644 index 00000000..5a81f596 --- /dev/null +++ b/src/writerai/types/shared_params/tool_call.py @@ -0,0 +1,23 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["ToolCall", "Function"] + + +class Function(TypedDict, total=False): + arguments: Required[str] + + name: str + + +class ToolCall(TypedDict, total=False): + id: Required[str] + + function: Required[Function] + + type: Required[str] + + index: int diff --git a/src/writerai/types/shared_params/tool_choice_json_object.py b/src/writerai/types/shared_params/tool_choice_json_object.py new file mode 100644 index 00000000..4b2cdbdc --- /dev/null +++ b/src/writerai/types/shared_params/tool_choice_json_object.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict +from typing_extensions import Required, TypedDict + +__all__ = ["ToolChoiceJsonObject"] + + +class ToolChoiceJsonObject(TypedDict, total=False): + value: Required[Dict[str, object]] diff --git a/src/writerai/types/shared_params/tool_choice_string.py b/src/writerai/types/shared_params/tool_choice_string.py new file mode 100644 index 00000000..5852b152 --- /dev/null +++ b/src/writerai/types/shared_params/tool_choice_string.py @@ -0,0 +1,11 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["ToolChoiceString"] + + +class ToolChoiceString(TypedDict, total=False): + value: Required[Literal["none", "auto", "required"]] diff --git a/src/writerai/types/shared_params/tool_param.py b/src/writerai/types/shared_params/tool_param.py new file mode 100644 index 00000000..851654c1 --- /dev/null +++ b/src/writerai/types/shared_params/tool_param.py @@ -0,0 +1,38 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Union +from typing_extensions import Literal, Required, TypeAlias, TypedDict + +from .function_definition import FunctionDefinition + +__all__ = ["ToolParam", "FunctionTool", "GraphTool", "GraphToolFunction"] + + +class FunctionTool(TypedDict, total=False): + function: Required[FunctionDefinition] + + type: Required[Literal["function"]] + """The type of tool.""" + + +class GraphToolFunction(TypedDict, total=False): + graph_ids: Required[List[str]] + """An array of graph IDs to be used in the tool.""" + + subqueries: Required[bool] + """Boolean to indicate whether to include subqueries in the response.""" + + description: str + """A description of the graph content.""" + + +class GraphTool(TypedDict, total=False): + function: Required[GraphToolFunction] + + type: Required[Literal["graph"]] + """The type of tool.""" + + +ToolParam: TypeAlias = Union[FunctionTool, GraphTool] diff --git a/tests/api_resources/test_chat.py b/tests/api_resources/test_chat.py index 03c27d4d..f581a9d0 100644 --- a/tests/api_resources/test_chat.py +++ b/tests/api_resources/test_chat.py @@ -9,7 +9,7 @@ from writerai import Writer, AsyncWriter from tests.utils import assert_matches_type -from writerai.types import Chat +from writerai.types import ChatCompletion base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -23,7 +23,7 @@ def test_method_chat_overload_1(self, client: Writer) -> None: messages=[{"role": "user"}], model="palmyra-x-004", ) - assert_matches_type(Chat, chat, path=["response"]) + assert_matches_type(ChatCompletion, chat, path=["response"]) @parametrize def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: @@ -90,7 +90,7 @@ def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: ], top_p=0, ) - assert_matches_type(Chat, chat, path=["response"]) + assert_matches_type(ChatCompletion, chat, path=["response"]) @parametrize def test_raw_response_chat_overload_1(self, client: Writer) -> None: @@ -102,7 +102,7 @@ def test_raw_response_chat_overload_1(self, client: Writer) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" chat = response.parse() - assert_matches_type(Chat, chat, path=["response"]) + assert_matches_type(ChatCompletion, chat, path=["response"]) @parametrize def test_streaming_response_chat_overload_1(self, client: Writer) -> None: @@ -114,7 +114,7 @@ def test_streaming_response_chat_overload_1(self, client: Writer) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" chat = response.parse() - assert_matches_type(Chat, chat, path=["response"]) + assert_matches_type(ChatCompletion, chat, path=["response"]) assert cast(Any, response.is_closed) is True @@ -231,7 +231,7 @@ async def test_method_chat_overload_1(self, async_client: AsyncWriter) -> None: messages=[{"role": "user"}], model="palmyra-x-004", ) - assert_matches_type(Chat, chat, path=["response"]) + assert_matches_type(ChatCompletion, chat, path=["response"]) @parametrize async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncWriter) -> None: @@ -298,7 +298,7 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW ], top_p=0, ) - assert_matches_type(Chat, chat, path=["response"]) + assert_matches_type(ChatCompletion, chat, path=["response"]) @parametrize async def test_raw_response_chat_overload_1(self, async_client: AsyncWriter) -> None: @@ -310,7 +310,7 @@ async def test_raw_response_chat_overload_1(self, async_client: AsyncWriter) -> assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" chat = await response.parse() - assert_matches_type(Chat, chat, path=["response"]) + assert_matches_type(ChatCompletion, chat, path=["response"]) @parametrize async def test_streaming_response_chat_overload_1(self, async_client: AsyncWriter) -> None: @@ -322,7 +322,7 @@ async def test_streaming_response_chat_overload_1(self, async_client: AsyncWrite assert response.http_request.headers.get("X-Stainless-Lang") == "python" chat = await response.parse() - assert_matches_type(Chat, chat, path=["response"]) + assert_matches_type(ChatCompletion, chat, path=["response"]) assert cast(Any, response.is_closed) is True From 99f194eed5042efb55f75e6c537489ae24eead0a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 28 Nov 2024 16:16:26 +0000 Subject: [PATCH 129/399] fix(client): compat with new httpx 0.28.0 release (#126) --- src/writerai/_base_client.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index 520a439c..3459f3b6 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -792,6 +792,7 @@ def __init__( custom_query: Mapping[str, object] | None = None, _strict_response_validation: bool, ) -> None: + kwargs: dict[str, Any] = {} if limits is not None: warnings.warn( "The `connection_pool_limits` argument is deprecated. The `http_client` argument should be passed instead", @@ -804,6 +805,7 @@ def __init__( limits = DEFAULT_CONNECTION_LIMITS if transport is not None: + kwargs["transport"] = transport warnings.warn( "The `transport` argument is deprecated. The `http_client` argument should be passed instead", category=DeprecationWarning, @@ -813,6 +815,7 @@ def __init__( raise ValueError("The `http_client` argument is mutually exclusive with `transport`") if proxies is not None: + kwargs["proxies"] = proxies warnings.warn( "The `proxies` argument is deprecated. The `http_client` argument should be passed instead", category=DeprecationWarning, @@ -856,10 +859,9 @@ def __init__( base_url=base_url, # cast to a valid type because mypy doesn't understand our type narrowing timeout=cast(Timeout, timeout), - proxies=proxies, - transport=transport, limits=limits, follow_redirects=True, + **kwargs, # type: ignore ) def is_closed(self) -> bool: @@ -1358,6 +1360,7 @@ def __init__( custom_headers: Mapping[str, str] | None = None, custom_query: Mapping[str, object] | None = None, ) -> None: + kwargs: dict[str, Any] = {} if limits is not None: warnings.warn( "The `connection_pool_limits` argument is deprecated. The `http_client` argument should be passed instead", @@ -1370,6 +1373,7 @@ def __init__( limits = DEFAULT_CONNECTION_LIMITS if transport is not None: + kwargs["transport"] = transport warnings.warn( "The `transport` argument is deprecated. The `http_client` argument should be passed instead", category=DeprecationWarning, @@ -1379,6 +1383,7 @@ def __init__( raise ValueError("The `http_client` argument is mutually exclusive with `transport`") if proxies is not None: + kwargs["proxies"] = proxies warnings.warn( "The `proxies` argument is deprecated. The `http_client` argument should be passed instead", category=DeprecationWarning, @@ -1422,10 +1427,9 @@ def __init__( base_url=base_url, # cast to a valid type because mypy doesn't understand our type narrowing timeout=cast(Timeout, timeout), - proxies=proxies, - transport=transport, limits=limits, follow_redirects=True, + **kwargs, # type: ignore ) def is_closed(self) -> bool: From f724ab8ccdc363c0777854a21345ff3282a7b2e1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 28 Nov 2024 20:35:10 +0000 Subject: [PATCH 130/399] feat(api): manual updates (#127) --- .stats.yml | 2 +- README.md | 8 +- api.md | 33 +-- src/writerai/_streaming.py | 6 - src/writerai/resources/chat.py | 35 ++- src/writerai/resources/completions.py | 18 +- src/writerai/types/__init__.py | 22 +- src/writerai/types/chat.py | 222 ++++++++++++++++++ src/writerai/types/chat_chat_params.py | 125 +++++++++- src/writerai/types/chat_completion.py | 54 ----- src/writerai/types/chat_completion_choice.py | 32 --- src/writerai/types/chat_completion_chunk.py | 222 +++++++++++++++++- src/writerai/types/chat_completion_message.py | 28 --- src/writerai/types/chat_completion_usage.py | 27 --- src/writerai/types/completion.py | 55 ++++- src/writerai/types/question.py | 21 +- src/writerai/types/shared/__init__.py | 15 -- src/writerai/types/shared/error_message.py | 15 -- src/writerai/types/shared/error_object.py | 16 -- .../types/shared/function_definition.py | 18 -- src/writerai/types/shared/function_params.py | 8 - src/writerai/types/shared/graph_data.py | 27 --- src/writerai/types/shared/logprobs.py | 14 -- src/writerai/types/shared/logprobs_token.py | 25 -- src/writerai/types/shared/source.py | 14 -- src/writerai/types/shared/tool_call.py | 23 -- .../types/shared/tool_call_streaming.py | 23 -- .../types/shared/tool_choice_json_object.py | 11 - .../types/shared/tool_choice_string.py | 11 - src/writerai/types/shared/tool_param.py | 37 --- src/writerai/types/shared_params/__init__.py | 10 - .../shared_params/function_definition.py | 19 -- .../types/shared_params/function_params.py | 10 - .../types/shared_params/graph_data.py | 28 --- src/writerai/types/shared_params/source.py | 15 -- src/writerai/types/shared_params/tool_call.py | 23 -- .../shared_params/tool_choice_json_object.py | 12 - .../types/shared_params/tool_choice_string.py | 11 - .../types/shared_params/tool_param.py | 38 --- ...{completion_chunk.py => streaming_data.py} | 4 +- tests/api_resources/test_chat.py | 18 +- 41 files changed, 664 insertions(+), 691 deletions(-) create mode 100644 src/writerai/types/chat.py delete mode 100644 src/writerai/types/chat_completion.py delete mode 100644 src/writerai/types/chat_completion_choice.py delete mode 100644 src/writerai/types/chat_completion_message.py delete mode 100644 src/writerai/types/chat_completion_usage.py delete mode 100644 src/writerai/types/shared/__init__.py delete mode 100644 src/writerai/types/shared/error_message.py delete mode 100644 src/writerai/types/shared/error_object.py delete mode 100644 src/writerai/types/shared/function_definition.py delete mode 100644 src/writerai/types/shared/function_params.py delete mode 100644 src/writerai/types/shared/graph_data.py delete mode 100644 src/writerai/types/shared/logprobs.py delete mode 100644 src/writerai/types/shared/logprobs_token.py delete mode 100644 src/writerai/types/shared/source.py delete mode 100644 src/writerai/types/shared/tool_call.py delete mode 100644 src/writerai/types/shared/tool_call_streaming.py delete mode 100644 src/writerai/types/shared/tool_choice_json_object.py delete mode 100644 src/writerai/types/shared/tool_choice_string.py delete mode 100644 src/writerai/types/shared/tool_param.py delete mode 100644 src/writerai/types/shared_params/__init__.py delete mode 100644 src/writerai/types/shared_params/function_definition.py delete mode 100644 src/writerai/types/shared_params/function_params.py delete mode 100644 src/writerai/types/shared_params/graph_data.py delete mode 100644 src/writerai/types/shared_params/source.py delete mode 100644 src/writerai/types/shared_params/tool_call.py delete mode 100644 src/writerai/types/shared_params/tool_choice_json_object.py delete mode 100644 src/writerai/types/shared_params/tool_choice_string.py delete mode 100644 src/writerai/types/shared_params/tool_param.py rename src/writerai/types/{completion_chunk.py => streaming_data.py} (68%) diff --git a/.stats.yml b/.stats.yml index 54ba5613..40f1ba49 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-6b363dd34169cab18f5ec3bcf6586aecd4799f79a80c90bf54e5a12f91d9e7c2.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-82683f2fd5f8778a27960ebabda40d6dc4640bdfb77ac4ec7f173b8bf8076d3c.yml diff --git a/README.md b/README.md index f54af779..ef552775 100644 --- a/README.md +++ b/README.md @@ -31,11 +31,11 @@ client = Writer( api_key=os.environ.get("WRITER_API_KEY"), # This is the default and can be omitted ) -chat_completion = client.chat.chat( +chat = client.chat.chat( messages=[{"role": "user"}], model="palmyra-x-004", ) -print(chat_completion.id) +print(chat.id) ``` While you can provide an `api_key` keyword argument, @@ -58,11 +58,11 @@ client = AsyncWriter( async def main() -> None: - chat_completion = await client.chat.chat( + chat = await client.chat.chat( messages=[{"role": "user"}], model="palmyra-x-004", ) - print(chat_completion.id) + print(chat.id) asyncio.run(main()) diff --git a/api.md b/api.md index 3c02faae..a86244a6 100644 --- a/api.md +++ b/api.md @@ -1,23 +1,3 @@ -# Shared Types - -```python -from writerai.types import ( - ErrorMessage, - ErrorObject, - FunctionDefinition, - FunctionParams, - GraphData, - Logprobs, - LogprobsToken, - Source, - ToolCall, - ToolCallStreaming, - ToolChoiceJsonObject, - ToolChoiceString, - ToolParam, -) -``` - # Applications Types: @@ -35,26 +15,19 @@ Methods: Types: ```python -from writerai.types import ( - ChatCompletion, - ChatCompletionChoice, - ChatCompletionChunk, - ChatCompletionMessage, - ChatCompletionParams, - ChatCompletionUsage, -) +from writerai.types import Chat, ChatCompletionChunk ``` Methods: -- client.chat.chat(\*\*params) -> ChatCompletion +- client.chat.chat(\*\*params) -> Chat # Completions Types: ```python -from writerai.types import Completion, CompletionChunk, CompletionParams +from writerai.types import Completion, StreamingData ``` Methods: diff --git a/src/writerai/_streaming.py b/src/writerai/_streaming.py index ca4711bc..794ea612 100644 --- a/src/writerai/_streaming.py +++ b/src/writerai/_streaming.py @@ -55,9 +55,6 @@ def __stream__(self) -> Iterator[_T]: iterator = self._iter_events() for sse in iterator: - if sse.data.startswith("[DONE]"): - break - if sse.event is None: yield process_data(data=sse.json(), cast_to=cast_to, response=response) @@ -138,9 +135,6 @@ async def __stream__(self) -> AsyncIterator[_T]: iterator = self._iter_events() async for sse in iterator: - if sse.data.startswith("[DONE]"): - break - if sse.event is None: yield process_data(data=sse.json(), cast_to=cast_to, response=response) diff --git a/src/writerai/resources/chat.py b/src/writerai/resources/chat.py index 7b7de07f..005aabad 100644 --- a/src/writerai/resources/chat.py +++ b/src/writerai/resources/chat.py @@ -23,10 +23,9 @@ async_to_streamed_response_wrapper, ) from .._streaming import Stream, AsyncStream +from ..types.chat import Chat from .._base_client import make_request_options -from ..types.chat_completion import ChatCompletion from ..types.chat_completion_chunk import ChatCompletionChunk -from ..types.shared_params.tool_param import ToolParam __all__ = ["ChatResource", "AsyncChatResource"] @@ -65,7 +64,7 @@ def chat( stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[ToolParam] | NotGiven = NOT_GIVEN, + tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -73,7 +72,7 @@ def chat( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> ChatCompletion: + ) -> Chat: """Generate a chat completion based on the provided messages. The response shown @@ -148,7 +147,7 @@ def chat( stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[ToolParam] | NotGiven = NOT_GIVEN, + tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -231,7 +230,7 @@ def chat( stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[ToolParam] | NotGiven = NOT_GIVEN, + tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -239,7 +238,7 @@ def chat( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> ChatCompletion | Stream[ChatCompletionChunk]: + ) -> Chat | Stream[ChatCompletionChunk]: """Generate a chat completion based on the provided messages. The response shown @@ -314,7 +313,7 @@ def chat( stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[ToolParam] | NotGiven = NOT_GIVEN, + tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -322,7 +321,7 @@ def chat( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> ChatCompletion | Stream[ChatCompletionChunk]: + ) -> Chat | Stream[ChatCompletionChunk]: return self._post( "/v1/chat", body=maybe_transform( @@ -345,7 +344,7 @@ def chat( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ChatCompletion, + cast_to=Chat, stream=stream or False, stream_cls=Stream[ChatCompletionChunk], ) @@ -385,7 +384,7 @@ async def chat( stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[ToolParam] | NotGiven = NOT_GIVEN, + tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -393,7 +392,7 @@ async def chat( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> ChatCompletion: + ) -> Chat: """Generate a chat completion based on the provided messages. The response shown @@ -468,7 +467,7 @@ async def chat( stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[ToolParam] | NotGiven = NOT_GIVEN, + tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -551,7 +550,7 @@ async def chat( stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[ToolParam] | NotGiven = NOT_GIVEN, + tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -559,7 +558,7 @@ async def chat( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> ChatCompletion | AsyncStream[ChatCompletionChunk]: + ) -> Chat | AsyncStream[ChatCompletionChunk]: """Generate a chat completion based on the provided messages. The response shown @@ -634,7 +633,7 @@ async def chat( stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[ToolParam] | NotGiven = NOT_GIVEN, + tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -642,7 +641,7 @@ async def chat( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> ChatCompletion | AsyncStream[ChatCompletionChunk]: + ) -> Chat | AsyncStream[ChatCompletionChunk]: return await self._post( "/v1/chat", body=await async_maybe_transform( @@ -665,7 +664,7 @@ async def chat( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ChatCompletion, + cast_to=Chat, stream=stream or False, stream_cls=AsyncStream[ChatCompletionChunk], ) diff --git a/src/writerai/resources/completions.py b/src/writerai/resources/completions.py index b25c80be..81162da0 100644 --- a/src/writerai/resources/completions.py +++ b/src/writerai/resources/completions.py @@ -25,7 +25,7 @@ from .._streaming import Stream, AsyncStream from .._base_client import make_request_options from ..types.completion import Completion -from ..types.completion_chunk import CompletionChunk +from ..types.streaming_data import StreamingData __all__ = ["CompletionsResource", "AsyncCompletionsResource"] @@ -130,7 +130,7 @@ def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Stream[CompletionChunk]: + ) -> Stream[StreamingData]: """ Text generation @@ -191,7 +191,7 @@ def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Completion | Stream[CompletionChunk]: + ) -> Completion | Stream[StreamingData]: """ Text generation @@ -252,7 +252,7 @@ def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Completion | Stream[CompletionChunk]: + ) -> Completion | Stream[StreamingData]: return self._post( "/v1/completions", body=maybe_transform( @@ -274,7 +274,7 @@ def create( ), cast_to=Completion, stream=stream or False, - stream_cls=Stream[CompletionChunk], + stream_cls=Stream[StreamingData], ) @@ -378,7 +378,7 @@ async def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AsyncStream[CompletionChunk]: + ) -> AsyncStream[StreamingData]: """ Text generation @@ -439,7 +439,7 @@ async def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Completion | AsyncStream[CompletionChunk]: + ) -> Completion | AsyncStream[StreamingData]: """ Text generation @@ -500,7 +500,7 @@ async def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Completion | AsyncStream[CompletionChunk]: + ) -> Completion | AsyncStream[StreamingData]: return await self._post( "/v1/completions", body=await async_maybe_transform( @@ -522,7 +522,7 @@ async def create( ), cast_to=Completion, stream=stream or False, - stream_cls=AsyncStream[CompletionChunk], + stream_cls=AsyncStream[StreamingData], ) diff --git a/src/writerai/types/__init__.py b/src/writerai/types/__init__.py index 1679d6bf..af6ad6c7 100644 --- a/src/writerai/types/__init__.py +++ b/src/writerai/types/__init__.py @@ -2,28 +2,13 @@ from __future__ import annotations +from .chat import Chat as Chat from .file import File as File from .graph import Graph as Graph -from .shared import ( - Source as Source, - Logprobs as Logprobs, - ToolCall as ToolCall, - GraphData as GraphData, - ToolParam as ToolParam, - ErrorObject as ErrorObject, - ErrorMessage as ErrorMessage, - LogprobsToken as LogprobsToken, - FunctionParams as FunctionParams, - ToolChoiceString as ToolChoiceString, - ToolCallStreaming as ToolCallStreaming, - FunctionDefinition as FunctionDefinition, - ToolChoiceJsonObject as ToolChoiceJsonObject, -) from .question import Question as Question from .completion import Completion as Completion -from .chat_completion import ChatCompletion as ChatCompletion +from .streaming_data import StreamingData as StreamingData from .chat_chat_params import ChatChatParams as ChatChatParams -from .completion_chunk import CompletionChunk as CompletionChunk from .file_list_params import FileListParams as FileListParams from .file_retry_params import FileRetryParams as FileRetryParams from .graph_list_params import GraphListParams as GraphListParams @@ -34,14 +19,11 @@ from .model_list_response import ModelListResponse as ModelListResponse from .file_delete_response import FileDeleteResponse as FileDeleteResponse from .chat_completion_chunk import ChatCompletionChunk as ChatCompletionChunk -from .chat_completion_usage import ChatCompletionUsage as ChatCompletionUsage from .graph_create_response import GraphCreateResponse as GraphCreateResponse from .graph_delete_response import GraphDeleteResponse as GraphDeleteResponse from .graph_question_params import GraphQuestionParams as GraphQuestionParams from .graph_update_response import GraphUpdateResponse as GraphUpdateResponse from .tool_parse_pdf_params import ToolParsePdfParams as ToolParsePdfParams -from .chat_completion_choice import ChatCompletionChoice as ChatCompletionChoice -from .chat_completion_message import ChatCompletionMessage as ChatCompletionMessage from .question_response_chunk import QuestionResponseChunk as QuestionResponseChunk from .tool_parse_pdf_response import ToolParsePdfResponse as ToolParsePdfResponse from .completion_create_params import CompletionCreateParams as CompletionCreateParams diff --git a/src/writerai/types/chat.py b/src/writerai/types/chat.py new file mode 100644 index 00000000..48f7fd73 --- /dev/null +++ b/src/writerai/types/chat.py @@ -0,0 +1,222 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = [ + "Chat", + "Choice", + "ChoiceMessage", + "ChoiceMessageGraphData", + "ChoiceMessageGraphDataSource", + "ChoiceMessageGraphDataSubquery", + "ChoiceMessageGraphDataSubquerySource", + "ChoiceMessageToolCall", + "ChoiceMessageToolCallFunction", + "ChoiceLogprobs", + "ChoiceLogprobsContent", + "ChoiceLogprobsContentTopLogprob", + "ChoiceLogprobsRefusal", + "ChoiceLogprobsRefusalTopLogprob", + "Usage", + "UsageCompletionTokensDetails", + "UsagePromptTokenDetails", +] + + +class ChoiceMessageGraphDataSource(BaseModel): + file_id: str + """The unique identifier of the file.""" + + snippet: str + """A snippet of text from the source file.""" + + +class ChoiceMessageGraphDataSubquerySource(BaseModel): + file_id: str + """The unique identifier of the file.""" + + snippet: str + """A snippet of text from the source file.""" + + +class ChoiceMessageGraphDataSubquery(BaseModel): + answer: str + """The answer to the subquery.""" + + query: str + """The subquery that was asked.""" + + sources: List[ChoiceMessageGraphDataSubquerySource] + + +class ChoiceMessageGraphData(BaseModel): + sources: Optional[List[ChoiceMessageGraphDataSource]] = None + + status: Optional[Literal["processing", "finished"]] = None + + subqueries: Optional[List[ChoiceMessageGraphDataSubquery]] = None + + +class ChoiceMessageToolCallFunction(BaseModel): + arguments: str + + name: str + + +class ChoiceMessageToolCall(BaseModel): + id: str + + function: ChoiceMessageToolCallFunction + + type: str + + index: Optional[int] = None + + +class ChoiceMessage(BaseModel): + content: str + """The text content produced by the model. + + This field contains the actual output generated, reflecting the model's response + to the input query or command. + """ + + refusal: Optional[str] = None + + role: Literal["assistant"] + """Specifies the role associated with the content.""" + + graph_data: Optional[ChoiceMessageGraphData] = None + + tool_calls: Optional[List[ChoiceMessageToolCall]] = None + + +class ChoiceLogprobsContentTopLogprob(BaseModel): + token: str + + logprob: float + + bytes: Optional[List[int]] = None + + +class ChoiceLogprobsContent(BaseModel): + token: str + + logprob: float + + top_logprobs: List[ChoiceLogprobsContentTopLogprob] + + bytes: Optional[List[int]] = None + + +class ChoiceLogprobsRefusalTopLogprob(BaseModel): + token: str + + logprob: float + + bytes: Optional[List[int]] = None + + +class ChoiceLogprobsRefusal(BaseModel): + token: str + + logprob: float + + top_logprobs: List[ChoiceLogprobsRefusalTopLogprob] + + bytes: Optional[List[int]] = None + + +class ChoiceLogprobs(BaseModel): + content: Optional[List[ChoiceLogprobsContent]] = None + + refusal: Optional[List[ChoiceLogprobsRefusal]] = None + + +class Choice(BaseModel): + finish_reason: Literal["stop", "length", "content_filter", "tool_calls"] + """Describes the condition under which the model ceased generating content. + + Common reasons include 'length' (reached the maximum output size), 'stop' + (encountered a stop sequence), 'content_filter' (harmful content filtered out), + or 'tool_calls' (encountered tool calls). + """ + + index: int + """The index of the choice in the list of completions generated by the model.""" + + message: ChoiceMessage + """The chat completion message from the model. + + Note: this field is deprecated for streaming. Use `delta` instead. + """ + + logprobs: Optional[ChoiceLogprobs] = None + """Log probability information for the choice.""" + + +class UsageCompletionTokensDetails(BaseModel): + reasoning_tokens: int + + +class UsagePromptTokenDetails(BaseModel): + cached_tokens: int + + +class Usage(BaseModel): + completion_tokens: int + + prompt_tokens: int + + total_tokens: int + + completion_tokens_details: Optional[UsageCompletionTokensDetails] = None + + prompt_token_details: Optional[UsagePromptTokenDetails] = None + + +class Chat(BaseModel): + id: str + """A globally unique identifier (UUID) for the response generated by the API. + + This ID can be used to reference the specific operation or transaction within + the system for tracking or debugging purposes. + """ + + choices: List[Choice] + """ + An array of objects representing the different outcomes or results produced by + the model based on the input provided. + """ + + created: int + """The Unix timestamp (in seconds) when the response was created. + + This timestamp can be used to verify the timing of the response relative to + other events or operations. + """ + + model: str + """Identifies the specific model used to generate the response.""" + + object: Literal["chat.completion"] + """ + The type of object returned, which is always `chat.completion` for chat + responses. + """ + + service_tier: Optional[str] = None + """The service tier used for processing the request.""" + + system_fingerprint: Optional[str] = None + """A string representing the backend configuration that the model runs with.""" + + usage: Optional[Usage] = None + """Usage information for the chat completion response. + + Please note that at this time Knowledge Graph tool usage is not included in this + object. + """ diff --git a/src/writerai/types/chat_chat_params.py b/src/writerai/types/chat_chat_params.py index 9216e4cf..4bf0acd7 100644 --- a/src/writerai/types/chat_chat_params.py +++ b/src/writerai/types/chat_chat_params.py @@ -2,20 +2,27 @@ from __future__ import annotations -from typing import List, Union, Iterable, Optional +from typing import Dict, List, Union, Iterable, Optional from typing_extensions import Literal, Required, TypeAlias, TypedDict -from .shared_params.tool_call import ToolCall -from .shared_params.graph_data import GraphData -from .shared_params.tool_param import ToolParam -from .shared_params.tool_choice_string import ToolChoiceString -from .shared_params.tool_choice_json_object import ToolChoiceJsonObject - __all__ = [ "ChatChatParamsBase", "Message", + "MessageGraphData", + "MessageGraphDataSource", + "MessageGraphDataSubquery", + "MessageGraphDataSubquerySource", + "MessageToolCall", + "MessageToolCallFunction", "StreamOptions", "ToolChoice", + "ToolChoiceStringToolChoice", + "ToolChoiceJsonObjectToolChoice", + "Tool", + "ToolFunctionTool", + "ToolFunctionToolFunction", + "ToolGraphTool", + "ToolGraphToolFunction", "ChatChatParamsNonStreaming", "ChatChatParamsStreaming", ] @@ -75,7 +82,7 @@ class ChatChatParamsBase(TypedDict, total=False): pass a specific previously defined function. """ - tools: Iterable[ToolParam] + tools: Iterable[Tool] """ An array of tools described to the model using JSON schema that the model can use to generate responses. Passing graph IDs will automatically use the @@ -91,12 +98,62 @@ class ChatChatParamsBase(TypedDict, total=False): """ +class MessageGraphDataSource(TypedDict, total=False): + file_id: Required[str] + """The unique identifier of the file.""" + + snippet: Required[str] + """A snippet of text from the source file.""" + + +class MessageGraphDataSubquerySource(TypedDict, total=False): + file_id: Required[str] + """The unique identifier of the file.""" + + snippet: Required[str] + """A snippet of text from the source file.""" + + +class MessageGraphDataSubquery(TypedDict, total=False): + answer: Required[str] + """The answer to the subquery.""" + + query: Required[str] + """The subquery that was asked.""" + + sources: Required[Iterable[MessageGraphDataSubquerySource]] + + +class MessageGraphData(TypedDict, total=False): + sources: Iterable[MessageGraphDataSource] + + status: Literal["processing", "finished"] + + subqueries: Iterable[MessageGraphDataSubquery] + + +class MessageToolCallFunction(TypedDict, total=False): + arguments: Required[str] + + name: Required[str] + + +class MessageToolCall(TypedDict, total=False): + id: Required[str] + + function: Required[MessageToolCallFunction] + + type: Required[str] + + index: int + + class Message(TypedDict, total=False): role: Required[Literal["user", "assistant", "system", "tool"]] content: Optional[str] - graph_data: Optional[GraphData] + graph_data: Optional[MessageGraphData] name: Optional[str] @@ -104,7 +161,7 @@ class Message(TypedDict, total=False): tool_call_id: Optional[str] - tool_calls: Optional[Iterable[ToolCall]] + tool_calls: Optional[Iterable[MessageToolCall]] class StreamOptions(TypedDict, total=False): @@ -112,7 +169,53 @@ class StreamOptions(TypedDict, total=False): """Indicate whether to include usage information.""" -ToolChoice: TypeAlias = Union[ToolChoiceString, ToolChoiceJsonObject] +class ToolChoiceStringToolChoice(TypedDict, total=False): + value: Required[Literal["none", "auto", "required"]] + + +class ToolChoiceJsonObjectToolChoice(TypedDict, total=False): + value: Required[Dict[str, object]] + + +ToolChoice: TypeAlias = Union[ToolChoiceStringToolChoice, ToolChoiceJsonObjectToolChoice] + + +class ToolFunctionToolFunction(TypedDict, total=False): + name: Required[str] + """Name of the function""" + + description: str + """Description of the function""" + + parameters: Dict[str, object] + + +class ToolFunctionTool(TypedDict, total=False): + function: Required[ToolFunctionToolFunction] + + type: Required[Literal["function"]] + """The type of tool.""" + + +class ToolGraphToolFunction(TypedDict, total=False): + graph_ids: Required[List[str]] + """An array of graph IDs to be used in the tool.""" + + subqueries: Required[bool] + """Boolean to indicate whether to include subqueries in the response.""" + + description: str + """A description of the graph content.""" + + +class ToolGraphTool(TypedDict, total=False): + function: Required[ToolGraphToolFunction] + + type: Required[Literal["graph"]] + """The type of tool.""" + + +Tool: TypeAlias = Union[ToolFunctionTool, ToolGraphTool] class ChatChatParamsNonStreaming(ChatChatParamsBase, total=False): diff --git a/src/writerai/types/chat_completion.py b/src/writerai/types/chat_completion.py deleted file mode 100644 index a7521fff..00000000 --- a/src/writerai/types/chat_completion.py +++ /dev/null @@ -1,54 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List, Optional -from typing_extensions import Literal - -from .._models import BaseModel -from .chat_completion_usage import ChatCompletionUsage -from .chat_completion_choice import ChatCompletionChoice - -__all__ = ["ChatCompletion"] - - -class ChatCompletion(BaseModel): - id: str - """A globally unique identifier (UUID) for the response generated by the API. - - This ID can be used to reference the specific operation or transaction within - the system for tracking or debugging purposes. - """ - - choices: List[ChatCompletionChoice] - """ - An array of objects representing the different outcomes or results produced by - the model based on the input provided. - """ - - created: int - """The Unix timestamp (in seconds) when the response was created. - - This timestamp can be used to verify the timing of the response relative to - other events or operations. - """ - - model: str - """Identifies the specific model used to generate the response.""" - - object: Literal["chat.completion"] - """ - The type of object returned, which is always `chat.completion` for chat - responses. - """ - - service_tier: Optional[str] = None - """The service tier used for processing the request.""" - - system_fingerprint: Optional[str] = None - """A string representing the backend configuration that the model runs with.""" - - usage: Optional[ChatCompletionUsage] = None - """Usage information for the chat completion response. - - Please note that at this time Knowledge Graph tool usage is not included in this - object. - """ diff --git a/src/writerai/types/chat_completion_choice.py b/src/writerai/types/chat_completion_choice.py deleted file mode 100644 index f96b5bee..00000000 --- a/src/writerai/types/chat_completion_choice.py +++ /dev/null @@ -1,32 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional -from typing_extensions import Literal - -from .._models import BaseModel -from .shared.logprobs import Logprobs -from .chat_completion_message import ChatCompletionMessage - -__all__ = ["ChatCompletionChoice"] - - -class ChatCompletionChoice(BaseModel): - finish_reason: Literal["stop", "length", "content_filter", "tool_calls"] - """Describes the condition under which the model ceased generating content. - - Common reasons include 'length' (reached the maximum output size), 'stop' - (encountered a stop sequence), 'content_filter' (harmful content filtered out), - or 'tool_calls' (encountered tool calls). - """ - - index: int - """The index of the choice in the list of completions generated by the model.""" - - message: ChatCompletionMessage - """The chat completion message from the model. - - Note: this field is deprecated for streaming. Use `delta` instead. - """ - - logprobs: Optional[Logprobs] = None - """Log probability information for the choice.""" diff --git a/src/writerai/types/chat_completion_chunk.py b/src/writerai/types/chat_completion_chunk.py index 681cc663..0a6fe71a 100644 --- a/src/writerai/types/chat_completion_chunk.py +++ b/src/writerai/types/chat_completion_chunk.py @@ -4,13 +4,83 @@ from typing_extensions import Literal from .._models import BaseModel -from .shared.logprobs import Logprobs -from .shared.graph_data import GraphData -from .chat_completion_usage import ChatCompletionUsage -from .chat_completion_message import ChatCompletionMessage -from .shared.tool_call_streaming import ToolCallStreaming -__all__ = ["ChatCompletionChunk", "Choice", "ChoiceDelta"] +__all__ = [ + "ChatCompletionChunk", + "Choice", + "ChoiceDelta", + "ChoiceDeltaGraphData", + "ChoiceDeltaGraphDataSource", + "ChoiceDeltaGraphDataSubquery", + "ChoiceDeltaGraphDataSubquerySource", + "ChoiceDeltaToolCall", + "ChoiceDeltaToolCallFunction", + "ChoiceLogprobs", + "ChoiceLogprobsContent", + "ChoiceLogprobsContentTopLogprob", + "ChoiceLogprobsRefusal", + "ChoiceLogprobsRefusalTopLogprob", + "ChoiceMessage", + "ChoiceMessageGraphData", + "ChoiceMessageGraphDataSource", + "ChoiceMessageGraphDataSubquery", + "ChoiceMessageGraphDataSubquerySource", + "ChoiceMessageToolCall", + "ChoiceMessageToolCallFunction", + "Usage", + "UsageCompletionTokensDetails", + "UsagePromptTokenDetails", +] + + +class ChoiceDeltaGraphDataSource(BaseModel): + file_id: str + """The unique identifier of the file.""" + + snippet: str + """A snippet of text from the source file.""" + + +class ChoiceDeltaGraphDataSubquerySource(BaseModel): + file_id: str + """The unique identifier of the file.""" + + snippet: str + """A snippet of text from the source file.""" + + +class ChoiceDeltaGraphDataSubquery(BaseModel): + answer: str + """The answer to the subquery.""" + + query: str + """The subquery that was asked.""" + + sources: List[ChoiceDeltaGraphDataSubquerySource] + + +class ChoiceDeltaGraphData(BaseModel): + sources: Optional[List[ChoiceDeltaGraphDataSource]] = None + + status: Optional[Literal["processing", "finished"]] = None + + subqueries: Optional[List[ChoiceDeltaGraphDataSubquery]] = None + + +class ChoiceDeltaToolCallFunction(BaseModel): + arguments: str + + name: str + + +class ChoiceDeltaToolCall(BaseModel): + index: int + + id: Optional[str] = None + + function: Optional[ChoiceDeltaToolCallFunction] = None + + type: Optional[str] = None class ChoiceDelta(BaseModel): @@ -21,7 +91,7 @@ class ChoiceDelta(BaseModel): to the input query or command. """ - graph_data: Optional[GraphData] = None + graph_data: Optional[ChoiceDeltaGraphData] = None refusal: Optional[str] = None @@ -32,7 +102,117 @@ class ChoiceDelta(BaseModel): output within the interaction flow. """ - tool_calls: Optional[List[ToolCallStreaming]] = None + tool_calls: Optional[List[ChoiceDeltaToolCall]] = None + + +class ChoiceLogprobsContentTopLogprob(BaseModel): + token: str + + logprob: float + + bytes: Optional[List[int]] = None + + +class ChoiceLogprobsContent(BaseModel): + token: str + + logprob: float + + top_logprobs: List[ChoiceLogprobsContentTopLogprob] + + bytes: Optional[List[int]] = None + + +class ChoiceLogprobsRefusalTopLogprob(BaseModel): + token: str + + logprob: float + + bytes: Optional[List[int]] = None + + +class ChoiceLogprobsRefusal(BaseModel): + token: str + + logprob: float + + top_logprobs: List[ChoiceLogprobsRefusalTopLogprob] + + bytes: Optional[List[int]] = None + + +class ChoiceLogprobs(BaseModel): + content: Optional[List[ChoiceLogprobsContent]] = None + + refusal: Optional[List[ChoiceLogprobsRefusal]] = None + + +class ChoiceMessageGraphDataSource(BaseModel): + file_id: str + """The unique identifier of the file.""" + + snippet: str + """A snippet of text from the source file.""" + + +class ChoiceMessageGraphDataSubquerySource(BaseModel): + file_id: str + """The unique identifier of the file.""" + + snippet: str + """A snippet of text from the source file.""" + + +class ChoiceMessageGraphDataSubquery(BaseModel): + answer: str + """The answer to the subquery.""" + + query: str + """The subquery that was asked.""" + + sources: List[ChoiceMessageGraphDataSubquerySource] + + +class ChoiceMessageGraphData(BaseModel): + sources: Optional[List[ChoiceMessageGraphDataSource]] = None + + status: Optional[Literal["processing", "finished"]] = None + + subqueries: Optional[List[ChoiceMessageGraphDataSubquery]] = None + + +class ChoiceMessageToolCallFunction(BaseModel): + arguments: str + + name: str + + +class ChoiceMessageToolCall(BaseModel): + id: str + + function: ChoiceMessageToolCallFunction + + type: str + + index: Optional[int] = None + + +class ChoiceMessage(BaseModel): + content: str + """The text content produced by the model. + + This field contains the actual output generated, reflecting the model's response + to the input query or command. + """ + + refusal: Optional[str] = None + + role: Literal["assistant"] + """Specifies the role associated with the content.""" + + graph_data: Optional[ChoiceMessageGraphData] = None + + tool_calls: Optional[List[ChoiceMessageToolCall]] = None class Choice(BaseModel): @@ -50,16 +230,36 @@ class Choice(BaseModel): index: int """The index of the choice in the list of completions generated by the model.""" - logprobs: Optional[Logprobs] = None + logprobs: Optional[ChoiceLogprobs] = None """Log probability information for the choice.""" - message: Optional[ChatCompletionMessage] = None + message: Optional[ChoiceMessage] = None """The chat completion message from the model. Note: this field is deprecated for streaming. Use `delta` instead. """ +class UsageCompletionTokensDetails(BaseModel): + reasoning_tokens: int + + +class UsagePromptTokenDetails(BaseModel): + cached_tokens: int + + +class Usage(BaseModel): + completion_tokens: int + + prompt_tokens: int + + total_tokens: int + + completion_tokens_details: Optional[UsageCompletionTokensDetails] = None + + prompt_token_details: Optional[UsagePromptTokenDetails] = None + + class ChatCompletionChunk(BaseModel): id: str """A globally unique identifier (UUID) for the response generated by the API. @@ -94,7 +294,7 @@ class ChatCompletionChunk(BaseModel): system_fingerprint: Optional[str] = None - usage: Optional[ChatCompletionUsage] = None + usage: Optional[Usage] = None """Usage information for the chat completion response. Please note that at this time Knowledge Graph tool usage is not included in this diff --git a/src/writerai/types/chat_completion_message.py b/src/writerai/types/chat_completion_message.py deleted file mode 100644 index 4187ea36..00000000 --- a/src/writerai/types/chat_completion_message.py +++ /dev/null @@ -1,28 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List, Optional -from typing_extensions import Literal - -from .._models import BaseModel -from .shared.tool_call import ToolCall -from .shared.graph_data import GraphData - -__all__ = ["ChatCompletionMessage"] - - -class ChatCompletionMessage(BaseModel): - content: str - """The text content produced by the model. - - This field contains the actual output generated, reflecting the model's response - to the input query or command. - """ - - refusal: Optional[str] = None - - role: Literal["assistant"] - """Specifies the role associated with the content.""" - - graph_data: Optional[GraphData] = None - - tool_calls: Optional[List[ToolCall]] = None diff --git a/src/writerai/types/chat_completion_usage.py b/src/writerai/types/chat_completion_usage.py deleted file mode 100644 index 6fc89018..00000000 --- a/src/writerai/types/chat_completion_usage.py +++ /dev/null @@ -1,27 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional - -from .._models import BaseModel - -__all__ = ["ChatCompletionUsage", "CompletionTokensDetails", "PromptTokenDetails"] - - -class CompletionTokensDetails(BaseModel): - reasoning_tokens: int - - -class PromptTokenDetails(BaseModel): - cached_tokens: int - - -class ChatCompletionUsage(BaseModel): - completion_tokens: int - - prompt_tokens: int - - total_tokens: int - - completion_tokens_details: Optional[CompletionTokensDetails] = None - - prompt_token_details: Optional[PromptTokenDetails] = None diff --git a/src/writerai/types/completion.py b/src/writerai/types/completion.py index 01fe40e5..5f4e82ae 100644 --- a/src/writerai/types/completion.py +++ b/src/writerai/types/completion.py @@ -3,9 +3,58 @@ from typing import List, Optional from .._models import BaseModel -from .shared.logprobs import Logprobs -__all__ = ["Completion", "Choice"] +__all__ = [ + "Completion", + "Choice", + "ChoiceLogProbs", + "ChoiceLogProbsContent", + "ChoiceLogProbsContentTopLogprob", + "ChoiceLogProbsRefusal", + "ChoiceLogProbsRefusalTopLogprob", +] + + +class ChoiceLogProbsContentTopLogprob(BaseModel): + token: str + + logprob: float + + bytes: Optional[List[int]] = None + + +class ChoiceLogProbsContent(BaseModel): + token: str + + logprob: float + + top_logprobs: List[ChoiceLogProbsContentTopLogprob] + + bytes: Optional[List[int]] = None + + +class ChoiceLogProbsRefusalTopLogprob(BaseModel): + token: str + + logprob: float + + bytes: Optional[List[int]] = None + + +class ChoiceLogProbsRefusal(BaseModel): + token: str + + logprob: float + + top_logprobs: List[ChoiceLogProbsRefusalTopLogprob] + + bytes: Optional[List[int]] = None + + +class ChoiceLogProbs(BaseModel): + content: Optional[List[ChoiceLogProbsContent]] = None + + refusal: Optional[List[ChoiceLogProbsRefusal]] = None class Choice(BaseModel): @@ -15,7 +64,7 @@ class Choice(BaseModel): response. """ - log_probs: Optional[Logprobs] = None + log_probs: Optional[ChoiceLogProbs] = None class Completion(BaseModel): diff --git a/src/writerai/types/question.py b/src/writerai/types/question.py index 4776dfbd..24473a47 100644 --- a/src/writerai/types/question.py +++ b/src/writerai/types/question.py @@ -3,9 +3,24 @@ from typing import List, Optional from .._models import BaseModel -from .shared.source import Source -__all__ = ["Question", "Subquery"] +__all__ = ["Question", "Source", "Subquery", "SubquerySource"] + + +class Source(BaseModel): + file_id: str + """The unique identifier of the file.""" + + snippet: str + """A snippet of text from the source file.""" + + +class SubquerySource(BaseModel): + file_id: str + """The unique identifier of the file.""" + + snippet: str + """A snippet of text from the source file.""" class Subquery(BaseModel): @@ -15,7 +30,7 @@ class Subquery(BaseModel): query: str """The subquery that was asked.""" - sources: List[Source] + sources: List[SubquerySource] class Question(BaseModel): diff --git a/src/writerai/types/shared/__init__.py b/src/writerai/types/shared/__init__.py deleted file mode 100644 index d62055d0..00000000 --- a/src/writerai/types/shared/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from .source import Source as Source -from .logprobs import Logprobs as Logprobs -from .tool_call import ToolCall as ToolCall -from .graph_data import GraphData as GraphData -from .tool_param import ToolParam as ToolParam -from .error_object import ErrorObject as ErrorObject -from .error_message import ErrorMessage as ErrorMessage -from .logprobs_token import LogprobsToken as LogprobsToken -from .function_params import FunctionParams as FunctionParams -from .tool_choice_string import ToolChoiceString as ToolChoiceString -from .function_definition import FunctionDefinition as FunctionDefinition -from .tool_call_streaming import ToolCallStreaming as ToolCallStreaming -from .tool_choice_json_object import ToolChoiceJsonObject as ToolChoiceJsonObject diff --git a/src/writerai/types/shared/error_message.py b/src/writerai/types/shared/error_message.py deleted file mode 100644 index 2b5b1599..00000000 --- a/src/writerai/types/shared/error_message.py +++ /dev/null @@ -1,15 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Dict - -from ..._models import BaseModel - -__all__ = ["ErrorMessage"] - - -class ErrorMessage(BaseModel): - description: str - - extras: Dict[str, object] - - key: str diff --git a/src/writerai/types/shared/error_object.py b/src/writerai/types/shared/error_object.py deleted file mode 100644 index e98f7a5e..00000000 --- a/src/writerai/types/shared/error_object.py +++ /dev/null @@ -1,16 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Dict, List - -from ..._models import BaseModel -from .error_message import ErrorMessage - -__all__ = ["ErrorObject"] - - -class ErrorObject(BaseModel): - errors: List[ErrorMessage] - - extras: Dict[str, object] - - tpe: str diff --git a/src/writerai/types/shared/function_definition.py b/src/writerai/types/shared/function_definition.py deleted file mode 100644 index c4f1e678..00000000 --- a/src/writerai/types/shared/function_definition.py +++ /dev/null @@ -1,18 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional - -from ..._models import BaseModel -from .function_params import FunctionParams - -__all__ = ["FunctionDefinition"] - - -class FunctionDefinition(BaseModel): - name: str - """Name of the function""" - - description: Optional[str] = None - """Description of the function""" - - parameters: Optional[FunctionParams] = None diff --git a/src/writerai/types/shared/function_params.py b/src/writerai/types/shared/function_params.py deleted file mode 100644 index cb00a506..00000000 --- a/src/writerai/types/shared/function_params.py +++ /dev/null @@ -1,8 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Dict -from typing_extensions import TypeAlias - -__all__ = ["FunctionParams"] - -FunctionParams: TypeAlias = Dict[str, object] diff --git a/src/writerai/types/shared/graph_data.py b/src/writerai/types/shared/graph_data.py deleted file mode 100644 index b0f21bfb..00000000 --- a/src/writerai/types/shared/graph_data.py +++ /dev/null @@ -1,27 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List, Optional -from typing_extensions import Literal - -from .source import Source -from ..._models import BaseModel - -__all__ = ["GraphData", "Subquery"] - - -class Subquery(BaseModel): - answer: str - """The answer to the subquery.""" - - query: str - """The subquery that was asked.""" - - sources: List[Source] - - -class GraphData(BaseModel): - sources: Optional[List[Source]] = None - - status: Optional[Literal["processing", "finished"]] = None - - subqueries: Optional[List[Subquery]] = None diff --git a/src/writerai/types/shared/logprobs.py b/src/writerai/types/shared/logprobs.py deleted file mode 100644 index 33dbf7ed..00000000 --- a/src/writerai/types/shared/logprobs.py +++ /dev/null @@ -1,14 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List, Optional - -from ..._models import BaseModel -from .logprobs_token import LogprobsToken - -__all__ = ["Logprobs"] - - -class Logprobs(BaseModel): - content: Optional[List[LogprobsToken]] = None - - refusal: Optional[List[LogprobsToken]] = None diff --git a/src/writerai/types/shared/logprobs_token.py b/src/writerai/types/shared/logprobs_token.py deleted file mode 100644 index 40c58d67..00000000 --- a/src/writerai/types/shared/logprobs_token.py +++ /dev/null @@ -1,25 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List, Optional - -from ..._models import BaseModel - -__all__ = ["LogprobsToken", "TopLogprob"] - - -class TopLogprob(BaseModel): - token: str - - logprob: float - - bytes: Optional[List[int]] = None - - -class LogprobsToken(BaseModel): - token: str - - logprob: float - - top_logprobs: List[TopLogprob] - - bytes: Optional[List[int]] = None diff --git a/src/writerai/types/shared/source.py b/src/writerai/types/shared/source.py deleted file mode 100644 index f737aa17..00000000 --- a/src/writerai/types/shared/source.py +++ /dev/null @@ -1,14 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - - -from ..._models import BaseModel - -__all__ = ["Source"] - - -class Source(BaseModel): - file_id: str - """The unique identifier of the file.""" - - snippet: str - """A snippet of text from the source file.""" diff --git a/src/writerai/types/shared/tool_call.py b/src/writerai/types/shared/tool_call.py deleted file mode 100644 index d21d0cc1..00000000 --- a/src/writerai/types/shared/tool_call.py +++ /dev/null @@ -1,23 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional - -from ..._models import BaseModel - -__all__ = ["ToolCall", "Function"] - - -class Function(BaseModel): - arguments: str - - name: Optional[str] = None - - -class ToolCall(BaseModel): - id: str - - function: Function - - type: str - - index: Optional[int] = None diff --git a/src/writerai/types/shared/tool_call_streaming.py b/src/writerai/types/shared/tool_call_streaming.py deleted file mode 100644 index 350001f2..00000000 --- a/src/writerai/types/shared/tool_call_streaming.py +++ /dev/null @@ -1,23 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional - -from ..._models import BaseModel - -__all__ = ["ToolCallStreaming", "Function"] - - -class Function(BaseModel): - arguments: str - - name: Optional[str] = None - - -class ToolCallStreaming(BaseModel): - index: int - - id: Optional[str] = None - - function: Optional[Function] = None - - type: Optional[str] = None diff --git a/src/writerai/types/shared/tool_choice_json_object.py b/src/writerai/types/shared/tool_choice_json_object.py deleted file mode 100644 index bc12acf5..00000000 --- a/src/writerai/types/shared/tool_choice_json_object.py +++ /dev/null @@ -1,11 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Dict - -from ..._models import BaseModel - -__all__ = ["ToolChoiceJsonObject"] - - -class ToolChoiceJsonObject(BaseModel): - value: Dict[str, object] diff --git a/src/writerai/types/shared/tool_choice_string.py b/src/writerai/types/shared/tool_choice_string.py deleted file mode 100644 index 653664da..00000000 --- a/src/writerai/types/shared/tool_choice_string.py +++ /dev/null @@ -1,11 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing_extensions import Literal - -from ..._models import BaseModel - -__all__ = ["ToolChoiceString"] - - -class ToolChoiceString(BaseModel): - value: Literal["none", "auto", "required"] diff --git a/src/writerai/types/shared/tool_param.py b/src/writerai/types/shared/tool_param.py deleted file mode 100644 index 93b5ac25..00000000 --- a/src/writerai/types/shared/tool_param.py +++ /dev/null @@ -1,37 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List, Union, Optional -from typing_extensions import Literal, TypeAlias - -from ..._models import BaseModel -from .function_definition import FunctionDefinition - -__all__ = ["ToolParam", "FunctionTool", "GraphTool", "GraphToolFunction"] - - -class FunctionTool(BaseModel): - function: FunctionDefinition - - type: Literal["function"] - """The type of tool.""" - - -class GraphToolFunction(BaseModel): - graph_ids: List[str] - """An array of graph IDs to be used in the tool.""" - - subqueries: bool - """Boolean to indicate whether to include subqueries in the response.""" - - description: Optional[str] = None - """A description of the graph content.""" - - -class GraphTool(BaseModel): - function: GraphToolFunction - - type: Literal["graph"] - """The type of tool.""" - - -ToolParam: TypeAlias = Union[FunctionTool, GraphTool] diff --git a/src/writerai/types/shared_params/__init__.py b/src/writerai/types/shared_params/__init__.py deleted file mode 100644 index 1bd920cf..00000000 --- a/src/writerai/types/shared_params/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from .source import Source as Source -from .tool_call import ToolCall as ToolCall -from .graph_data import GraphData as GraphData -from .tool_param import ToolParam as ToolParam -from .function_params import FunctionParams as FunctionParams -from .tool_choice_string import ToolChoiceString as ToolChoiceString -from .function_definition import FunctionDefinition as FunctionDefinition -from .tool_choice_json_object import ToolChoiceJsonObject as ToolChoiceJsonObject diff --git a/src/writerai/types/shared_params/function_definition.py b/src/writerai/types/shared_params/function_definition.py deleted file mode 100644 index 4043fa70..00000000 --- a/src/writerai/types/shared_params/function_definition.py +++ /dev/null @@ -1,19 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Required, TypedDict - -from .function_params import FunctionParams - -__all__ = ["FunctionDefinition"] - - -class FunctionDefinition(TypedDict, total=False): - name: Required[str] - """Name of the function""" - - description: str - """Description of the function""" - - parameters: FunctionParams diff --git a/src/writerai/types/shared_params/function_params.py b/src/writerai/types/shared_params/function_params.py deleted file mode 100644 index dd1f1c2a..00000000 --- a/src/writerai/types/shared_params/function_params.py +++ /dev/null @@ -1,10 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Dict -from typing_extensions import TypeAlias - -__all__ = ["FunctionParams"] - -FunctionParams: TypeAlias = Dict[str, object] diff --git a/src/writerai/types/shared_params/graph_data.py b/src/writerai/types/shared_params/graph_data.py deleted file mode 100644 index d89894c0..00000000 --- a/src/writerai/types/shared_params/graph_data.py +++ /dev/null @@ -1,28 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Iterable -from typing_extensions import Literal, Required, TypedDict - -from .source import Source - -__all__ = ["GraphData", "Subquery"] - - -class Subquery(TypedDict, total=False): - answer: Required[str] - """The answer to the subquery.""" - - query: Required[str] - """The subquery that was asked.""" - - sources: Required[Iterable[Source]] - - -class GraphData(TypedDict, total=False): - sources: Iterable[Source] - - status: Literal["processing", "finished"] - - subqueries: Iterable[Subquery] diff --git a/src/writerai/types/shared_params/source.py b/src/writerai/types/shared_params/source.py deleted file mode 100644 index 54b70059..00000000 --- a/src/writerai/types/shared_params/source.py +++ /dev/null @@ -1,15 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Required, TypedDict - -__all__ = ["Source"] - - -class Source(TypedDict, total=False): - file_id: Required[str] - """The unique identifier of the file.""" - - snippet: Required[str] - """A snippet of text from the source file.""" diff --git a/src/writerai/types/shared_params/tool_call.py b/src/writerai/types/shared_params/tool_call.py deleted file mode 100644 index 5a81f596..00000000 --- a/src/writerai/types/shared_params/tool_call.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 - -__all__ = ["ToolCall", "Function"] - - -class Function(TypedDict, total=False): - arguments: Required[str] - - name: str - - -class ToolCall(TypedDict, total=False): - id: Required[str] - - function: Required[Function] - - type: Required[str] - - index: int diff --git a/src/writerai/types/shared_params/tool_choice_json_object.py b/src/writerai/types/shared_params/tool_choice_json_object.py deleted file mode 100644 index 4b2cdbdc..00000000 --- a/src/writerai/types/shared_params/tool_choice_json_object.py +++ /dev/null @@ -1,12 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Dict -from typing_extensions import Required, TypedDict - -__all__ = ["ToolChoiceJsonObject"] - - -class ToolChoiceJsonObject(TypedDict, total=False): - value: Required[Dict[str, object]] diff --git a/src/writerai/types/shared_params/tool_choice_string.py b/src/writerai/types/shared_params/tool_choice_string.py deleted file mode 100644 index 5852b152..00000000 --- a/src/writerai/types/shared_params/tool_choice_string.py +++ /dev/null @@ -1,11 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Literal, Required, TypedDict - -__all__ = ["ToolChoiceString"] - - -class ToolChoiceString(TypedDict, total=False): - value: Required[Literal["none", "auto", "required"]] diff --git a/src/writerai/types/shared_params/tool_param.py b/src/writerai/types/shared_params/tool_param.py deleted file mode 100644 index 851654c1..00000000 --- a/src/writerai/types/shared_params/tool_param.py +++ /dev/null @@ -1,38 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import List, Union -from typing_extensions import Literal, Required, TypeAlias, TypedDict - -from .function_definition import FunctionDefinition - -__all__ = ["ToolParam", "FunctionTool", "GraphTool", "GraphToolFunction"] - - -class FunctionTool(TypedDict, total=False): - function: Required[FunctionDefinition] - - type: Required[Literal["function"]] - """The type of tool.""" - - -class GraphToolFunction(TypedDict, total=False): - graph_ids: Required[List[str]] - """An array of graph IDs to be used in the tool.""" - - subqueries: Required[bool] - """Boolean to indicate whether to include subqueries in the response.""" - - description: str - """A description of the graph content.""" - - -class GraphTool(TypedDict, total=False): - function: Required[GraphToolFunction] - - type: Required[Literal["graph"]] - """The type of tool.""" - - -ToolParam: TypeAlias = Union[FunctionTool, GraphTool] diff --git a/src/writerai/types/completion_chunk.py b/src/writerai/types/streaming_data.py similarity index 68% rename from src/writerai/types/completion_chunk.py rename to src/writerai/types/streaming_data.py index aec58bd3..8c88b456 100644 --- a/src/writerai/types/completion_chunk.py +++ b/src/writerai/types/streaming_data.py @@ -3,8 +3,8 @@ from .._models import BaseModel -__all__ = ["CompletionChunk"] +__all__ = ["StreamingData"] -class CompletionChunk(BaseModel): +class StreamingData(BaseModel): value: str diff --git a/tests/api_resources/test_chat.py b/tests/api_resources/test_chat.py index f581a9d0..03c27d4d 100644 --- a/tests/api_resources/test_chat.py +++ b/tests/api_resources/test_chat.py @@ -9,7 +9,7 @@ from writerai import Writer, AsyncWriter from tests.utils import assert_matches_type -from writerai.types import ChatCompletion +from writerai.types import Chat base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -23,7 +23,7 @@ def test_method_chat_overload_1(self, client: Writer) -> None: messages=[{"role": "user"}], model="palmyra-x-004", ) - assert_matches_type(ChatCompletion, chat, path=["response"]) + assert_matches_type(Chat, chat, path=["response"]) @parametrize def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: @@ -90,7 +90,7 @@ def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: ], top_p=0, ) - assert_matches_type(ChatCompletion, chat, path=["response"]) + assert_matches_type(Chat, chat, path=["response"]) @parametrize def test_raw_response_chat_overload_1(self, client: Writer) -> None: @@ -102,7 +102,7 @@ def test_raw_response_chat_overload_1(self, client: Writer) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" chat = response.parse() - assert_matches_type(ChatCompletion, chat, path=["response"]) + assert_matches_type(Chat, chat, path=["response"]) @parametrize def test_streaming_response_chat_overload_1(self, client: Writer) -> None: @@ -114,7 +114,7 @@ def test_streaming_response_chat_overload_1(self, client: Writer) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" chat = response.parse() - assert_matches_type(ChatCompletion, chat, path=["response"]) + assert_matches_type(Chat, chat, path=["response"]) assert cast(Any, response.is_closed) is True @@ -231,7 +231,7 @@ async def test_method_chat_overload_1(self, async_client: AsyncWriter) -> None: messages=[{"role": "user"}], model="palmyra-x-004", ) - assert_matches_type(ChatCompletion, chat, path=["response"]) + assert_matches_type(Chat, chat, path=["response"]) @parametrize async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncWriter) -> None: @@ -298,7 +298,7 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW ], top_p=0, ) - assert_matches_type(ChatCompletion, chat, path=["response"]) + assert_matches_type(Chat, chat, path=["response"]) @parametrize async def test_raw_response_chat_overload_1(self, async_client: AsyncWriter) -> None: @@ -310,7 +310,7 @@ async def test_raw_response_chat_overload_1(self, async_client: AsyncWriter) -> assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" chat = await response.parse() - assert_matches_type(ChatCompletion, chat, path=["response"]) + assert_matches_type(Chat, chat, path=["response"]) @parametrize async def test_streaming_response_chat_overload_1(self, async_client: AsyncWriter) -> None: @@ -322,7 +322,7 @@ async def test_streaming_response_chat_overload_1(self, async_client: AsyncWrite assert response.http_request.headers.get("X-Stainless-Lang") == "python" chat = await response.parse() - assert_matches_type(ChatCompletion, chat, path=["response"]) + assert_matches_type(Chat, chat, path=["response"]) assert cast(Any, response.is_closed) is True From d550ed4e4a6c69ad3f4a763b15af9de51d2efcfe Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 29 Nov 2024 14:39:47 +0000 Subject: [PATCH 131/399] chore(internal): version bump (#128) --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 3e9af1b3..fbd9082d 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.4.0" + ".": "1.5.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index fb6d60d5..0fd9458a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "1.4.0" +version = "1.5.0" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index 7f493856..20200b4c 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "1.4.0" # x-release-please-version +__version__ = "1.5.0" # x-release-please-version From 0787ac4d96b8caa41cbd54d4cd1c04a0ad0c84dd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 20:43:49 +0000 Subject: [PATCH 132/399] chore(internal): bump pyright (#129) --- requirements-dev.lock | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements-dev.lock b/requirements-dev.lock index 0bd4c5da..489e8b2c 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -68,7 +68,7 @@ pydantic-core==2.23.4 # via pydantic pygments==2.18.0 # via rich -pyright==1.1.380 +pyright==1.1.389 pytest==8.3.3 # via pytest-asyncio pytest-asyncio==0.24.0 @@ -96,6 +96,7 @@ typing-extensions==4.12.2 # via mypy # via pydantic # via pydantic-core + # via pyright # via writer-sdk virtualenv==20.24.5 # via nox From fb7d532f0f01194427d67ebda4ea77225354c78c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 3 Dec 2024 00:16:12 +0000 Subject: [PATCH 133/399] feat(api): api update (#131) --- .stats.yml | 2 +- src/writerai/resources/applications.py | 24 +++++++++++++--- .../application_generate_content_params.py | 6 ++++ tests/api_resources/test_applications.py | 28 +++++++++++++++++++ 4 files changed, 55 insertions(+), 5 deletions(-) diff --git a/.stats.yml b/.stats.yml index 40f1ba49..dcd7ed21 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-82683f2fd5f8778a27960ebabda40d6dc4640bdfb77ac4ec7f173b8bf8076d3c.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-fcd4d82943d0aeefc300520f0ee4684456ef647140f1d6ba9ffcb86278d83d3a.yml diff --git a/src/writerai/resources/applications.py b/src/writerai/resources/applications.py index a9f031fd..acd3daed 100644 --- a/src/writerai/resources/applications.py +++ b/src/writerai/resources/applications.py @@ -51,6 +51,7 @@ def generate_content( application_id: str, *, inputs: Iterable[application_generate_content_params.Input], + stream: bool | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -59,9 +60,12 @@ def generate_content( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ApplicationGenerateContentResponse: """ - Generate content from an existing application with inputs. + Generate content from an existing no-code application with inputs. Args: + stream: Indicates whether the response should be streamed. Currently only supported for + research assistant applications. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -75,7 +79,11 @@ def generate_content( return self._post( f"/v1/applications/{application_id}", body=maybe_transform( - {"inputs": inputs}, application_generate_content_params.ApplicationGenerateContentParams + { + "inputs": inputs, + "stream": stream, + }, + application_generate_content_params.ApplicationGenerateContentParams, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -109,6 +117,7 @@ async def generate_content( application_id: str, *, inputs: Iterable[application_generate_content_params.Input], + stream: bool | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -117,9 +126,12 @@ async def generate_content( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ApplicationGenerateContentResponse: """ - Generate content from an existing application with inputs. + Generate content from an existing no-code application with inputs. Args: + stream: Indicates whether the response should be streamed. Currently only supported for + research assistant applications. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -133,7 +145,11 @@ async def generate_content( return await self._post( f"/v1/applications/{application_id}", body=await async_maybe_transform( - {"inputs": inputs}, application_generate_content_params.ApplicationGenerateContentParams + { + "inputs": inputs, + "stream": stream, + }, + application_generate_content_params.ApplicationGenerateContentParams, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout diff --git a/src/writerai/types/application_generate_content_params.py b/src/writerai/types/application_generate_content_params.py index 2b93046b..1a1d933b 100644 --- a/src/writerai/types/application_generate_content_params.py +++ b/src/writerai/types/application_generate_content_params.py @@ -11,6 +11,12 @@ class ApplicationGenerateContentParams(TypedDict, total=False): inputs: Required[Iterable[Input]] + stream: bool + """Indicates whether the response should be streamed. + + Currently only supported for research assistant applications. + """ + class Input(TypedDict, total=False): id: Required[str] diff --git a/tests/api_resources/test_applications.py b/tests/api_resources/test_applications.py index cdf9faa2..7a93c41f 100644 --- a/tests/api_resources/test_applications.py +++ b/tests/api_resources/test_applications.py @@ -30,6 +30,20 @@ def test_method_generate_content(self, client: Writer) -> None: ) assert_matches_type(ApplicationGenerateContentResponse, application, path=["response"]) + @parametrize + def test_method_generate_content_with_all_params(self, client: Writer) -> None: + application = client.applications.generate_content( + application_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + inputs=[ + { + "id": "id", + "value": ["string"], + } + ], + stream=True, + ) + assert_matches_type(ApplicationGenerateContentResponse, application, path=["response"]) + @parametrize def test_raw_response_generate_content(self, client: Writer) -> None: response = client.applications.with_raw_response.generate_content( @@ -96,6 +110,20 @@ async def test_method_generate_content(self, async_client: AsyncWriter) -> None: ) assert_matches_type(ApplicationGenerateContentResponse, application, path=["response"]) + @parametrize + async def test_method_generate_content_with_all_params(self, async_client: AsyncWriter) -> None: + application = await async_client.applications.generate_content( + application_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + inputs=[ + { + "id": "id", + "value": ["string"], + } + ], + stream=True, + ) + assert_matches_type(ApplicationGenerateContentResponse, application, path=["response"]) + @parametrize async def test_raw_response_generate_content(self, async_client: AsyncWriter) -> None: response = await async_client.applications.with_raw_response.generate_content( From d84a785e879458c2d73a8b694daa2153370fa6cf Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 3 Dec 2024 17:26:16 +0000 Subject: [PATCH 134/399] feat(api): add streaming to application generation (#132) --- api.md | 2 +- src/writerai/resources/applications.py | 170 +++++++++++++++++- src/writerai/types/__init__.py | 1 + .../application_generate_content_chunk.py | 33 ++++ .../application_generate_content_params.py | 40 +++-- tests/api_resources/test_applications.py | 154 ++++++++++++++-- 6 files changed, 375 insertions(+), 25 deletions(-) create mode 100644 src/writerai/types/application_generate_content_chunk.py diff --git a/api.md b/api.md index a86244a6..15740a86 100644 --- a/api.md +++ b/api.md @@ -3,7 +3,7 @@ Types: ```python -from writerai.types import ApplicationGenerateContentResponse +from writerai.types import ApplicationGenerateContentChunk, ApplicationGenerateContentResponse ``` Methods: diff --git a/src/writerai/resources/applications.py b/src/writerai/resources/applications.py index acd3daed..2f8afa3b 100644 --- a/src/writerai/resources/applications.py +++ b/src/writerai/resources/applications.py @@ -3,12 +3,14 @@ from __future__ import annotations from typing import Iterable +from typing_extensions import Literal, overload import httpx from ..types import application_generate_content_params from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven from .._utils import ( + required_args, maybe_transform, async_maybe_transform, ) @@ -20,7 +22,9 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) +from .._streaming import Stream, AsyncStream from .._base_client import make_request_options +from ..types.application_generate_content_chunk import ApplicationGenerateContentChunk from ..types.application_generate_content_response import ApplicationGenerateContentResponse __all__ = ["ApplicationsResource", "AsyncApplicationsResource"] @@ -46,12 +50,13 @@ def with_streaming_response(self) -> ApplicationsResourceWithStreamingResponse: """ return ApplicationsResourceWithStreamingResponse(self) + @overload def generate_content( self, application_id: str, *, inputs: Iterable[application_generate_content_params.Input], - stream: bool | NotGiven = NOT_GIVEN, + stream: Literal[False] | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -74,6 +79,84 @@ def generate_content( timeout: Override the client-level default timeout for this request, in seconds """ + ... + + @overload + def generate_content( + self, + application_id: str, + *, + inputs: Iterable[application_generate_content_params.Input], + stream: Literal[True], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Stream[ApplicationGenerateContentChunk]: + """ + Generate content from an existing no-code application with inputs. + + Args: + stream: Indicates whether the response should be streamed. Currently only supported for + research assistant applications. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + def generate_content( + self, + application_id: str, + *, + inputs: Iterable[application_generate_content_params.Input], + stream: bool, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ApplicationGenerateContentResponse | Stream[ApplicationGenerateContentChunk]: + """ + Generate content from an existing no-code application with inputs. + + Args: + stream: Indicates whether the response should be streamed. Currently only supported for + research assistant applications. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @required_args(["inputs"], ["inputs", "stream"]) + def generate_content( + self, + application_id: str, + *, + inputs: Iterable[application_generate_content_params.Input], + stream: Literal[False] | Literal[True] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ApplicationGenerateContentResponse | Stream[ApplicationGenerateContentChunk]: if not application_id: raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") return self._post( @@ -89,6 +172,8 @@ def generate_content( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=ApplicationGenerateContentResponse, + stream=stream or False, + stream_cls=Stream[ApplicationGenerateContentChunk], ) @@ -112,12 +197,13 @@ def with_streaming_response(self) -> AsyncApplicationsResourceWithStreamingRespo """ return AsyncApplicationsResourceWithStreamingResponse(self) + @overload async def generate_content( self, application_id: str, *, inputs: Iterable[application_generate_content_params.Input], - stream: bool | NotGiven = NOT_GIVEN, + stream: Literal[False] | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -140,6 +226,84 @@ async def generate_content( timeout: Override the client-level default timeout for this request, in seconds """ + ... + + @overload + async def generate_content( + self, + application_id: str, + *, + inputs: Iterable[application_generate_content_params.Input], + stream: Literal[True], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncStream[ApplicationGenerateContentChunk]: + """ + Generate content from an existing no-code application with inputs. + + Args: + stream: Indicates whether the response should be streamed. Currently only supported for + research assistant applications. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @overload + async def generate_content( + self, + application_id: str, + *, + inputs: Iterable[application_generate_content_params.Input], + stream: bool, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ApplicationGenerateContentResponse | AsyncStream[ApplicationGenerateContentChunk]: + """ + Generate content from an existing no-code application with inputs. + + Args: + stream: Indicates whether the response should be streamed. Currently only supported for + research assistant applications. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + ... + + @required_args(["inputs"], ["inputs", "stream"]) + async def generate_content( + self, + application_id: str, + *, + inputs: Iterable[application_generate_content_params.Input], + stream: Literal[False] | Literal[True] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ApplicationGenerateContentResponse | AsyncStream[ApplicationGenerateContentChunk]: if not application_id: raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") return await self._post( @@ -155,6 +319,8 @@ async def generate_content( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=ApplicationGenerateContentResponse, + stream=stream or False, + stream_cls=AsyncStream[ApplicationGenerateContentChunk], ) diff --git a/src/writerai/types/__init__.py b/src/writerai/types/__init__.py index af6ad6c7..cdbbd67c 100644 --- a/src/writerai/types/__init__.py +++ b/src/writerai/types/__init__.py @@ -28,6 +28,7 @@ from .tool_parse_pdf_response import ToolParsePdfResponse as ToolParsePdfResponse from .completion_create_params import CompletionCreateParams as CompletionCreateParams from .graph_add_file_to_graph_params import GraphAddFileToGraphParams as GraphAddFileToGraphParams +from .application_generate_content_chunk import ApplicationGenerateContentChunk as ApplicationGenerateContentChunk from .application_generate_content_params import ApplicationGenerateContentParams as ApplicationGenerateContentParams from .tool_context_aware_splitting_params import ToolContextAwareSplittingParams as ToolContextAwareSplittingParams from .application_generate_content_response import ( diff --git a/src/writerai/types/application_generate_content_chunk.py b/src/writerai/types/application_generate_content_chunk.py new file mode 100644 index 00000000..70e96939 --- /dev/null +++ b/src/writerai/types/application_generate_content_chunk.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .._models import BaseModel + +__all__ = ["ApplicationGenerateContentChunk", "Delta", "DeltaStage"] + + +class DeltaStage(BaseModel): + id: str + """The unique identifier for the stage.""" + + content: str + """The text content of the stage.""" + + sources: Optional[List[str]] = None + """A list of sources (URLs) that that stage used to process that particular step.""" + + +class Delta(BaseModel): + content: Optional[str] = None + """The main text output.""" + + stages: Optional[List[DeltaStage]] = None + """A list of stages that show the 'thinking process'.""" + + title: Optional[str] = None + """The name of the output.""" + + +class ApplicationGenerateContentChunk(BaseModel): + delta: Delta diff --git a/src/writerai/types/application_generate_content_params.py b/src/writerai/types/application_generate_content_params.py index 1a1d933b..885a2beb 100644 --- a/src/writerai/types/application_generate_content_params.py +++ b/src/writerai/types/application_generate_content_params.py @@ -2,21 +2,20 @@ from __future__ import annotations -from typing import List, Iterable -from typing_extensions import Required, TypedDict +from typing import List, Union, Iterable +from typing_extensions import Literal, Required, TypedDict -__all__ = ["ApplicationGenerateContentParams", "Input"] +__all__ = [ + "ApplicationGenerateContentParamsBase", + "Input", + "ApplicationGenerateContentParamsNonStreaming", + "ApplicationGenerateContentParamsStreaming", +] -class ApplicationGenerateContentParams(TypedDict, total=False): +class ApplicationGenerateContentParamsBase(TypedDict, total=False): inputs: Required[Iterable[Input]] - stream: bool - """Indicates whether the response should be streamed. - - Currently only supported for research assistant applications. - """ - class Input(TypedDict, total=False): id: Required[str] @@ -34,3 +33,24 @@ class Input(TypedDict, total=False): [here](https://dev.writer.com/api-guides/api-reference/file-api/upload-files) for the Files API. """ + + +class ApplicationGenerateContentParamsNonStreaming(ApplicationGenerateContentParamsBase, total=False): + stream: Literal[False] + """Indicates whether the response should be streamed. + + Currently only supported for research assistant applications. + """ + + +class ApplicationGenerateContentParamsStreaming(ApplicationGenerateContentParamsBase): + stream: Required[Literal[True]] + """Indicates whether the response should be streamed. + + Currently only supported for research assistant applications. + """ + + +ApplicationGenerateContentParams = Union[ + ApplicationGenerateContentParamsNonStreaming, ApplicationGenerateContentParamsStreaming +] diff --git a/tests/api_resources/test_applications.py b/tests/api_resources/test_applications.py index 7a93c41f..6d5e45af 100644 --- a/tests/api_resources/test_applications.py +++ b/tests/api_resources/test_applications.py @@ -18,7 +18,7 @@ class TestApplications: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) @parametrize - def test_method_generate_content(self, client: Writer) -> None: + def test_method_generate_content_overload_1(self, client: Writer) -> None: application = client.applications.generate_content( application_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", inputs=[ @@ -31,7 +31,7 @@ def test_method_generate_content(self, client: Writer) -> None: assert_matches_type(ApplicationGenerateContentResponse, application, path=["response"]) @parametrize - def test_method_generate_content_with_all_params(self, client: Writer) -> None: + def test_method_generate_content_with_all_params_overload_1(self, client: Writer) -> None: application = client.applications.generate_content( application_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", inputs=[ @@ -40,12 +40,12 @@ def test_method_generate_content_with_all_params(self, client: Writer) -> None: "value": ["string"], } ], - stream=True, + stream=False, ) assert_matches_type(ApplicationGenerateContentResponse, application, path=["response"]) @parametrize - def test_raw_response_generate_content(self, client: Writer) -> None: + def test_raw_response_generate_content_overload_1(self, client: Writer) -> None: response = client.applications.with_raw_response.generate_content( application_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", inputs=[ @@ -62,7 +62,7 @@ def test_raw_response_generate_content(self, client: Writer) -> None: assert_matches_type(ApplicationGenerateContentResponse, application, path=["response"]) @parametrize - def test_streaming_response_generate_content(self, client: Writer) -> None: + def test_streaming_response_generate_content_overload_1(self, client: Writer) -> None: with client.applications.with_streaming_response.generate_content( application_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", inputs=[ @@ -81,7 +81,71 @@ def test_streaming_response_generate_content(self, client: Writer) -> None: assert cast(Any, response.is_closed) is True @parametrize - def test_path_params_generate_content(self, client: Writer) -> None: + def test_path_params_generate_content_overload_1(self, client: Writer) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `application_id` but received ''"): + client.applications.with_raw_response.generate_content( + application_id="", + inputs=[ + { + "id": "id", + "value": ["string"], + } + ], + ) + + @parametrize + def test_method_generate_content_overload_2(self, client: Writer) -> None: + application_stream = client.applications.generate_content( + application_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + inputs=[ + { + "id": "id", + "value": ["string"], + } + ], + stream=True, + ) + application_stream.response.close() + + @parametrize + def test_raw_response_generate_content_overload_2(self, client: Writer) -> None: + response = client.applications.with_raw_response.generate_content( + application_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + inputs=[ + { + "id": "id", + "value": ["string"], + } + ], + stream=True, + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = response.parse() + stream.close() + + @parametrize + def test_streaming_response_generate_content_overload_2(self, client: Writer) -> None: + with client.applications.with_streaming_response.generate_content( + application_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + inputs=[ + { + "id": "id", + "value": ["string"], + } + ], + stream=True, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = response.parse() + stream.close() + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_generate_content_overload_2(self, client: Writer) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `application_id` but received ''"): client.applications.with_raw_response.generate_content( application_id="", @@ -91,6 +155,7 @@ def test_path_params_generate_content(self, client: Writer) -> None: "value": ["string"], } ], + stream=True, ) @@ -98,7 +163,7 @@ class TestAsyncApplications: parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) @parametrize - async def test_method_generate_content(self, async_client: AsyncWriter) -> None: + async def test_method_generate_content_overload_1(self, async_client: AsyncWriter) -> None: application = await async_client.applications.generate_content( application_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", inputs=[ @@ -111,7 +176,7 @@ async def test_method_generate_content(self, async_client: AsyncWriter) -> None: assert_matches_type(ApplicationGenerateContentResponse, application, path=["response"]) @parametrize - async def test_method_generate_content_with_all_params(self, async_client: AsyncWriter) -> None: + async def test_method_generate_content_with_all_params_overload_1(self, async_client: AsyncWriter) -> None: application = await async_client.applications.generate_content( application_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", inputs=[ @@ -120,12 +185,12 @@ async def test_method_generate_content_with_all_params(self, async_client: Async "value": ["string"], } ], - stream=True, + stream=False, ) assert_matches_type(ApplicationGenerateContentResponse, application, path=["response"]) @parametrize - async def test_raw_response_generate_content(self, async_client: AsyncWriter) -> None: + async def test_raw_response_generate_content_overload_1(self, async_client: AsyncWriter) -> None: response = await async_client.applications.with_raw_response.generate_content( application_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", inputs=[ @@ -142,7 +207,7 @@ async def test_raw_response_generate_content(self, async_client: AsyncWriter) -> assert_matches_type(ApplicationGenerateContentResponse, application, path=["response"]) @parametrize - async def test_streaming_response_generate_content(self, async_client: AsyncWriter) -> None: + async def test_streaming_response_generate_content_overload_1(self, async_client: AsyncWriter) -> None: async with async_client.applications.with_streaming_response.generate_content( application_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", inputs=[ @@ -161,7 +226,71 @@ async def test_streaming_response_generate_content(self, async_client: AsyncWrit assert cast(Any, response.is_closed) is True @parametrize - async def test_path_params_generate_content(self, async_client: AsyncWriter) -> None: + async def test_path_params_generate_content_overload_1(self, async_client: AsyncWriter) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `application_id` but received ''"): + await async_client.applications.with_raw_response.generate_content( + application_id="", + inputs=[ + { + "id": "id", + "value": ["string"], + } + ], + ) + + @parametrize + async def test_method_generate_content_overload_2(self, async_client: AsyncWriter) -> None: + application_stream = await async_client.applications.generate_content( + application_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + inputs=[ + { + "id": "id", + "value": ["string"], + } + ], + stream=True, + ) + await application_stream.response.aclose() + + @parametrize + async def test_raw_response_generate_content_overload_2(self, async_client: AsyncWriter) -> None: + response = await async_client.applications.with_raw_response.generate_content( + application_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + inputs=[ + { + "id": "id", + "value": ["string"], + } + ], + stream=True, + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = await response.parse() + await stream.close() + + @parametrize + async def test_streaming_response_generate_content_overload_2(self, async_client: AsyncWriter) -> None: + async with async_client.applications.with_streaming_response.generate_content( + application_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + inputs=[ + { + "id": "id", + "value": ["string"], + } + ], + stream=True, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = await response.parse() + await stream.close() + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_generate_content_overload_2(self, async_client: AsyncWriter) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `application_id` but received ''"): await async_client.applications.with_raw_response.generate_content( application_id="", @@ -171,4 +300,5 @@ async def test_path_params_generate_content(self, async_client: AsyncWriter) -> "value": ["string"], } ], + stream=True, ) From 9cda6d303a280d487ecb6e04f144ffc1f1d18626 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 4 Dec 2024 02:13:17 +0000 Subject: [PATCH 135/399] chore: make the `Omit` type public (#133) --- src/writerai/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/writerai/__init__.py b/src/writerai/__init__.py index cc6410c2..77e4947c 100644 --- a/src/writerai/__init__.py +++ b/src/writerai/__init__.py @@ -1,7 +1,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from . import types -from ._types import NOT_GIVEN, NoneType, NotGiven, Transport, ProxiesTypes +from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes from ._utils import file_from_path from ._client import Client, Stream, Writer, Timeout, Transport, AsyncClient, AsyncStream, AsyncWriter, RequestOptions from ._models import BaseModel @@ -36,6 +36,7 @@ "ProxiesTypes", "NotGiven", "NOT_GIVEN", + "Omit", "WriterError", "APIError", "APIStatusError", From 31858eb4eb6ece85c8bc354cb59b45c32169426e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 14:37:55 +0000 Subject: [PATCH 136/399] chore(internal): bump pydantic dependency (#134) --- requirements-dev.lock | 4 ++-- requirements.lock | 4 ++-- src/writerai/_types.py | 6 ++---- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/requirements-dev.lock b/requirements-dev.lock index 489e8b2c..8fc53054 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -62,9 +62,9 @@ platformdirs==3.11.0 # via virtualenv pluggy==1.5.0 # via pytest -pydantic==2.9.2 +pydantic==2.10.3 # via writer-sdk -pydantic-core==2.23.4 +pydantic-core==2.27.1 # via pydantic pygments==2.18.0 # via rich diff --git a/requirements.lock b/requirements.lock index 0091397a..4ddaea3d 100644 --- a/requirements.lock +++ b/requirements.lock @@ -30,9 +30,9 @@ httpx==0.25.2 idna==3.4 # via anyio # via httpx -pydantic==2.9.2 +pydantic==2.10.3 # via writer-sdk -pydantic-core==2.23.4 +pydantic-core==2.27.1 # via pydantic sniffio==1.3.0 # via anyio diff --git a/src/writerai/_types.py b/src/writerai/_types.py index 20b15b83..1b0929e6 100644 --- a/src/writerai/_types.py +++ b/src/writerai/_types.py @@ -192,10 +192,8 @@ def get(self, __key: str) -> str | None: ... StrBytesIntFloat = Union[str, bytes, int, float] # Note: copied from Pydantic -# https://github.com/pydantic/pydantic/blob/32ea570bf96e84234d2992e1ddf40ab8a565925a/pydantic/main.py#L49 -IncEx: TypeAlias = Union[ - Set[int], Set[str], Mapping[int, Union["IncEx", Literal[True]]], Mapping[str, Union["IncEx", Literal[True]]] -] +# https://github.com/pydantic/pydantic/blob/6f31f8f68ef011f84357330186f603ff295312fd/pydantic/main.py#L79 +IncEx: TypeAlias = Union[Set[int], Set[str], Mapping[int, Union["IncEx", bool]], Mapping[str, Union["IncEx", bool]]] PostParser = Callable[[Any], Any] From 8d3ee0422b2bb139b5e8682ef7409bdce91f9b92 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 18:17:18 +0000 Subject: [PATCH 137/399] docs(readme): fix http client proxies example (#135) --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ef552775..a6af2c47 100644 --- a/README.md +++ b/README.md @@ -382,18 +382,19 @@ can also get all the extra fields on the Pydantic model as a dict with You can directly override the [httpx client](https://www.python-httpx.org/api/#client) to customize it for your use case, including: -- Support for proxies -- Custom transports +- Support for [proxies](https://www.python-httpx.org/advanced/proxies/) +- Custom [transports](https://www.python-httpx.org/advanced/transports/) - Additional [advanced](https://www.python-httpx.org/advanced/clients/) functionality ```python +import httpx from writerai import Writer, DefaultHttpxClient client = Writer( # Or use the `WRITER_BASE_URL` env var base_url="http://my.test.server.example.com:8083", http_client=DefaultHttpxClient( - proxies="http://my.test.proxy.example.com", + proxy="http://my.test.proxy.example.com", transport=httpx.HTTPTransport(local_address="0.0.0.0"), ), ) From b7fa511d79052f1dc66e25d6b5b3edfe1677a4eb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 12:24:14 +0000 Subject: [PATCH 138/399] chore(internal): bump pyright (#136) --- requirements-dev.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.lock b/requirements-dev.lock index 8fc53054..8609015b 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -68,7 +68,7 @@ pydantic-core==2.27.1 # via pydantic pygments==2.18.0 # via rich -pyright==1.1.389 +pyright==1.1.390 pytest==8.3.3 # via pytest-asyncio pytest-asyncio==0.24.0 From 6c3041c4e9bd388a1cfb1d59783a120197126d29 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 12:37:07 +0000 Subject: [PATCH 139/399] chore(internal): add support for TypeAliasType (#137) --- pyproject.toml | 2 +- src/writerai/_models.py | 3 +++ src/writerai/_response.py | 20 ++++++++++---------- src/writerai/_utils/__init__.py | 1 + src/writerai/_utils/_typing.py | 31 ++++++++++++++++++++++++++++++- tests/test_models.py | 18 +++++++++++++++++- tests/utils.py | 4 ++++ 7 files changed, 66 insertions(+), 13 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0fd9458a..50e88726 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ authors = [ dependencies = [ "httpx>=0.23.0, <1", "pydantic>=1.9.0, <3", - "typing-extensions>=4.7, <5", + "typing-extensions>=4.10, <5", "anyio>=3.5.0, <5", "distro>=1.7.0, <2", "sniffio", diff --git a/src/writerai/_models.py b/src/writerai/_models.py index 6cb469e2..7a547ce5 100644 --- a/src/writerai/_models.py +++ b/src/writerai/_models.py @@ -46,6 +46,7 @@ strip_not_given, extract_type_arg, is_annotated_type, + is_type_alias_type, strip_annotated_type, ) from ._compat import ( @@ -428,6 +429,8 @@ def construct_type(*, value: object, type_: object) -> object: # we allow `object` as the input type because otherwise, passing things like # `Literal['value']` will be reported as a type error by type checkers type_ = cast("type[object]", type_) + if is_type_alias_type(type_): + type_ = type_.__value__ # type: ignore[unreachable] # unwrap `Annotated[T, ...]` -> `T` if is_annotated_type(type_): diff --git a/src/writerai/_response.py b/src/writerai/_response.py index 3a1356d7..a7f0b0cc 100644 --- a/src/writerai/_response.py +++ b/src/writerai/_response.py @@ -25,7 +25,7 @@ import pydantic from ._types import NoneType -from ._utils import is_given, extract_type_arg, is_annotated_type, extract_type_var_from_base +from ._utils import is_given, extract_type_arg, is_annotated_type, is_type_alias_type, extract_type_var_from_base from ._models import BaseModel, is_basemodel from ._constants import RAW_RESPONSE_HEADER, OVERRIDE_CAST_TO_HEADER from ._streaming import Stream, AsyncStream, is_stream_class_type, extract_stream_chunk_type @@ -126,9 +126,15 @@ def __repr__(self) -> str: ) def _parse(self, *, to: type[_T] | None = None) -> R | _T: + cast_to = to if to is not None else self._cast_to + + # unwrap `TypeAlias('Name', T)` -> `T` + if is_type_alias_type(cast_to): + cast_to = cast_to.__value__ # type: ignore[unreachable] + # unwrap `Annotated[T, ...]` -> `T` - if to and is_annotated_type(to): - to = extract_type_arg(to, 0) + if cast_to and is_annotated_type(cast_to): + cast_to = extract_type_arg(cast_to, 0) if self._is_sse_stream: if to: @@ -164,18 +170,12 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: return cast( R, stream_cls( - cast_to=self._cast_to, + cast_to=cast_to, response=self.http_response, client=cast(Any, self._client), ), ) - cast_to = to if to is not None else self._cast_to - - # unwrap `Annotated[T, ...]` -> `T` - if is_annotated_type(cast_to): - cast_to = extract_type_arg(cast_to, 0) - if cast_to is NoneType: return cast(R, None) diff --git a/src/writerai/_utils/__init__.py b/src/writerai/_utils/__init__.py index a7cff3c0..d4fda26f 100644 --- a/src/writerai/_utils/__init__.py +++ b/src/writerai/_utils/__init__.py @@ -39,6 +39,7 @@ is_iterable_type as is_iterable_type, is_required_type as is_required_type, is_annotated_type as is_annotated_type, + is_type_alias_type as is_type_alias_type, strip_annotated_type as strip_annotated_type, extract_type_var_from_base as extract_type_var_from_base, ) diff --git a/src/writerai/_utils/_typing.py b/src/writerai/_utils/_typing.py index c036991f..278749b1 100644 --- a/src/writerai/_utils/_typing.py +++ b/src/writerai/_utils/_typing.py @@ -1,8 +1,17 @@ from __future__ import annotations +import sys +import typing +import typing_extensions from typing import Any, TypeVar, Iterable, cast from collections import abc as _c_abc -from typing_extensions import Required, Annotated, get_args, get_origin +from typing_extensions import ( + TypeIs, + Required, + Annotated, + get_args, + get_origin, +) from .._types import InheritsGeneric from .._compat import is_union as _is_union @@ -36,6 +45,26 @@ def is_typevar(typ: type) -> bool: return type(typ) == TypeVar # type: ignore +_TYPE_ALIAS_TYPES: tuple[type[typing_extensions.TypeAliasType], ...] = (typing_extensions.TypeAliasType,) +if sys.version_info >= (3, 12): + _TYPE_ALIAS_TYPES = (*_TYPE_ALIAS_TYPES, typing.TypeAliasType) + + +def is_type_alias_type(tp: Any, /) -> TypeIs[typing_extensions.TypeAliasType]: + """Return whether the provided argument is an instance of `TypeAliasType`. + + ```python + type Int = int + is_type_alias_type(Int) + # > True + Str = TypeAliasType("Str", str) + is_type_alias_type(Str) + # > True + ``` + """ + return isinstance(tp, _TYPE_ALIAS_TYPES) + + # Extracts T from Annotated[T, ...] or from Required[Annotated[T, ...]] def strip_annotated_type(typ: type) -> type: if is_required_type(typ) or is_annotated_type(typ): diff --git a/tests/test_models.py b/tests/test_models.py index 2d00c359..46788052 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,7 +1,7 @@ import json from typing import Any, Dict, List, Union, Optional, cast from datetime import datetime, timezone -from typing_extensions import Literal, Annotated +from typing_extensions import Literal, Annotated, TypeAliasType import pytest import pydantic @@ -828,3 +828,19 @@ class B(BaseModel): # if the discriminator details object stays the same between invocations then # we hit the cache assert UnionType.__discriminator__ is discriminator + + +@pytest.mark.skipif(not PYDANTIC_V2, reason="TypeAliasType is not supported in Pydantic v1") +def test_type_alias_type() -> None: + Alias = TypeAliasType("Alias", str) + + class Model(BaseModel): + alias: Alias + union: Union[int, Alias] + + m = construct_type(value={"alias": "foo", "union": "bar"}, type_=Model) + assert isinstance(m, Model) + assert isinstance(m.alias, str) + assert m.alias == "foo" + assert isinstance(m.union, str) + assert m.union == "bar" diff --git a/tests/utils.py b/tests/utils.py index 741b4af6..64664db4 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -16,6 +16,7 @@ is_union_type, extract_type_arg, is_annotated_type, + is_type_alias_type, ) from writerai._compat import PYDANTIC_V2, field_outer_type, get_model_fields from writerai._models import BaseModel @@ -51,6 +52,9 @@ def assert_matches_type( path: list[str], allow_none: bool = False, ) -> None: + if is_type_alias_type(type_): + type_ = type_.__value__ + # unwrap `Annotated[T, ...]` -> `T` if is_annotated_type(type_): type_ = extract_type_arg(type_, 0) From 99c617affff1f3110c64fc42638c3faf3d4cd33a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 13 Dec 2024 13:58:56 +0000 Subject: [PATCH 140/399] chore(internal): updated imports (#138) --- src/writerai/_client.py | 128 +++++++++++++++++++--------------------- 1 file changed, 60 insertions(+), 68 deletions(-) diff --git a/src/writerai/_client.py b/src/writerai/_client.py index 0cbecbb5..8585f5b9 100644 --- a/src/writerai/_client.py +++ b/src/writerai/_client.py @@ -8,7 +8,7 @@ import httpx -from . import resources, _exceptions +from . import _exceptions from ._qs import Querystring from ._types import ( NOT_GIVEN, @@ -24,6 +24,7 @@ get_async_library, ) from ._version import __version__ +from .resources import chat, files, graphs, models, completions, applications from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import WriterError, APIStatusError from ._base_client import ( @@ -31,28 +32,19 @@ SyncAPIClient, AsyncAPIClient, ) +from .resources.tools import tools -__all__ = [ - "Timeout", - "Transport", - "ProxiesTypes", - "RequestOptions", - "resources", - "Writer", - "AsyncWriter", - "Client", - "AsyncClient", -] +__all__ = ["Timeout", "Transport", "ProxiesTypes", "RequestOptions", "Writer", "AsyncWriter", "Client", "AsyncClient"] class Writer(SyncAPIClient): - applications: resources.ApplicationsResource - chat: resources.ChatResource - completions: resources.CompletionsResource - models: resources.ModelsResource - graphs: resources.GraphsResource - files: resources.FilesResource - tools: resources.ToolsResource + applications: applications.ApplicationsResource + chat: chat.ChatResource + completions: completions.CompletionsResource + models: models.ModelsResource + graphs: graphs.GraphsResource + files: files.FilesResource + tools: tools.ToolsResource with_raw_response: WriterWithRawResponse with_streaming_response: WriterWithStreamedResponse @@ -112,13 +104,13 @@ def __init__( self._default_stream_cls = Stream - self.applications = resources.ApplicationsResource(self) - self.chat = resources.ChatResource(self) - self.completions = resources.CompletionsResource(self) - self.models = resources.ModelsResource(self) - self.graphs = resources.GraphsResource(self) - self.files = resources.FilesResource(self) - self.tools = resources.ToolsResource(self) + self.applications = applications.ApplicationsResource(self) + self.chat = chat.ChatResource(self) + self.completions = completions.CompletionsResource(self) + self.models = models.ModelsResource(self) + self.graphs = graphs.GraphsResource(self) + self.files = files.FilesResource(self) + self.tools = tools.ToolsResource(self) self.with_raw_response = WriterWithRawResponse(self) self.with_streaming_response = WriterWithStreamedResponse(self) @@ -228,13 +220,13 @@ def _make_status_error( class AsyncWriter(AsyncAPIClient): - applications: resources.AsyncApplicationsResource - chat: resources.AsyncChatResource - completions: resources.AsyncCompletionsResource - models: resources.AsyncModelsResource - graphs: resources.AsyncGraphsResource - files: resources.AsyncFilesResource - tools: resources.AsyncToolsResource + applications: applications.AsyncApplicationsResource + chat: chat.AsyncChatResource + completions: completions.AsyncCompletionsResource + models: models.AsyncModelsResource + graphs: graphs.AsyncGraphsResource + files: files.AsyncFilesResource + tools: tools.AsyncToolsResource with_raw_response: AsyncWriterWithRawResponse with_streaming_response: AsyncWriterWithStreamedResponse @@ -294,13 +286,13 @@ def __init__( self._default_stream_cls = AsyncStream - self.applications = resources.AsyncApplicationsResource(self) - self.chat = resources.AsyncChatResource(self) - self.completions = resources.AsyncCompletionsResource(self) - self.models = resources.AsyncModelsResource(self) - self.graphs = resources.AsyncGraphsResource(self) - self.files = resources.AsyncFilesResource(self) - self.tools = resources.AsyncToolsResource(self) + self.applications = applications.AsyncApplicationsResource(self) + self.chat = chat.AsyncChatResource(self) + self.completions = completions.AsyncCompletionsResource(self) + self.models = models.AsyncModelsResource(self) + self.graphs = graphs.AsyncGraphsResource(self) + self.files = files.AsyncFilesResource(self) + self.tools = tools.AsyncToolsResource(self) self.with_raw_response = AsyncWriterWithRawResponse(self) self.with_streaming_response = AsyncWriterWithStreamedResponse(self) @@ -411,46 +403,46 @@ def _make_status_error( class WriterWithRawResponse: def __init__(self, client: Writer) -> None: - self.applications = resources.ApplicationsResourceWithRawResponse(client.applications) - self.chat = resources.ChatResourceWithRawResponse(client.chat) - self.completions = resources.CompletionsResourceWithRawResponse(client.completions) - self.models = resources.ModelsResourceWithRawResponse(client.models) - self.graphs = resources.GraphsResourceWithRawResponse(client.graphs) - self.files = resources.FilesResourceWithRawResponse(client.files) - self.tools = resources.ToolsResourceWithRawResponse(client.tools) + self.applications = applications.ApplicationsResourceWithRawResponse(client.applications) + self.chat = chat.ChatResourceWithRawResponse(client.chat) + self.completions = completions.CompletionsResourceWithRawResponse(client.completions) + self.models = models.ModelsResourceWithRawResponse(client.models) + self.graphs = graphs.GraphsResourceWithRawResponse(client.graphs) + self.files = files.FilesResourceWithRawResponse(client.files) + self.tools = tools.ToolsResourceWithRawResponse(client.tools) class AsyncWriterWithRawResponse: def __init__(self, client: AsyncWriter) -> None: - self.applications = resources.AsyncApplicationsResourceWithRawResponse(client.applications) - self.chat = resources.AsyncChatResourceWithRawResponse(client.chat) - self.completions = resources.AsyncCompletionsResourceWithRawResponse(client.completions) - self.models = resources.AsyncModelsResourceWithRawResponse(client.models) - self.graphs = resources.AsyncGraphsResourceWithRawResponse(client.graphs) - self.files = resources.AsyncFilesResourceWithRawResponse(client.files) - self.tools = resources.AsyncToolsResourceWithRawResponse(client.tools) + self.applications = applications.AsyncApplicationsResourceWithRawResponse(client.applications) + self.chat = chat.AsyncChatResourceWithRawResponse(client.chat) + self.completions = completions.AsyncCompletionsResourceWithRawResponse(client.completions) + self.models = models.AsyncModelsResourceWithRawResponse(client.models) + self.graphs = graphs.AsyncGraphsResourceWithRawResponse(client.graphs) + self.files = files.AsyncFilesResourceWithRawResponse(client.files) + self.tools = tools.AsyncToolsResourceWithRawResponse(client.tools) class WriterWithStreamedResponse: def __init__(self, client: Writer) -> None: - self.applications = resources.ApplicationsResourceWithStreamingResponse(client.applications) - self.chat = resources.ChatResourceWithStreamingResponse(client.chat) - self.completions = resources.CompletionsResourceWithStreamingResponse(client.completions) - self.models = resources.ModelsResourceWithStreamingResponse(client.models) - self.graphs = resources.GraphsResourceWithStreamingResponse(client.graphs) - self.files = resources.FilesResourceWithStreamingResponse(client.files) - self.tools = resources.ToolsResourceWithStreamingResponse(client.tools) + self.applications = applications.ApplicationsResourceWithStreamingResponse(client.applications) + self.chat = chat.ChatResourceWithStreamingResponse(client.chat) + self.completions = completions.CompletionsResourceWithStreamingResponse(client.completions) + self.models = models.ModelsResourceWithStreamingResponse(client.models) + self.graphs = graphs.GraphsResourceWithStreamingResponse(client.graphs) + self.files = files.FilesResourceWithStreamingResponse(client.files) + self.tools = tools.ToolsResourceWithStreamingResponse(client.tools) class AsyncWriterWithStreamedResponse: def __init__(self, client: AsyncWriter) -> None: - self.applications = resources.AsyncApplicationsResourceWithStreamingResponse(client.applications) - self.chat = resources.AsyncChatResourceWithStreamingResponse(client.chat) - self.completions = resources.AsyncCompletionsResourceWithStreamingResponse(client.completions) - self.models = resources.AsyncModelsResourceWithStreamingResponse(client.models) - self.graphs = resources.AsyncGraphsResourceWithStreamingResponse(client.graphs) - self.files = resources.AsyncFilesResourceWithStreamingResponse(client.files) - self.tools = resources.AsyncToolsResourceWithStreamingResponse(client.tools) + self.applications = applications.AsyncApplicationsResourceWithStreamingResponse(client.applications) + self.chat = chat.AsyncChatResourceWithStreamingResponse(client.chat) + self.completions = completions.AsyncCompletionsResourceWithStreamingResponse(client.completions) + self.models = models.AsyncModelsResourceWithStreamingResponse(client.models) + self.graphs = graphs.AsyncGraphsResourceWithStreamingResponse(client.graphs) + self.files = files.AsyncFilesResourceWithStreamingResponse(client.files) + self.tools = tools.AsyncToolsResourceWithStreamingResponse(client.tools) Client = Writer From e811f8de8074b47afcaf7c201769bc0fed1bd9d5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:47:50 +0000 Subject: [PATCH 141/399] docs(readme): example snippet for client context manager (#139) --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index a6af2c47..25bf57e1 100644 --- a/README.md +++ b/README.md @@ -410,6 +410,16 @@ client.with_options(http_client=DefaultHttpxClient(...)) By default the library closes underlying HTTP connections whenever the client is [garbage collected](https://docs.python.org/3/reference/datamodel.html#object.__del__). You can manually close the client using the `.close()` method if desired, or with a context manager that closes when exiting. +```py +from writerai import Writer + +with Writer() as client: + # make requests here + ... + +# HTTP client is now closed +``` + ## Versioning This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions: From 363641789fa145d45d308dffdbc3eb1510968922 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 17:22:56 +0000 Subject: [PATCH 142/399] chore(internal): codegen related update (#140) --- src/writerai/resources/files.py | 6 +++--- src/writerai/types/file_upload_params.py | 3 ++- tests/api_resources/test_files.py | 12 ++++++------ 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/writerai/resources/files.py b/src/writerai/resources/files.py index fa601519..d4a1cd5c 100644 --- a/src/writerai/resources/files.py +++ b/src/writerai/resources/files.py @@ -8,7 +8,7 @@ import httpx from ..types import file_list_params, file_retry_params, file_upload_params -from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes from .._utils import ( maybe_transform, async_maybe_transform, @@ -264,7 +264,7 @@ def retry( def upload( self, *, - content: object, + content: FileTypes, content_disposition: str, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -522,7 +522,7 @@ async def retry( async def upload( self, *, - content: object, + content: FileTypes, content_disposition: str, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. diff --git a/src/writerai/types/file_upload_params.py b/src/writerai/types/file_upload_params.py index e8c05d3a..760021f9 100644 --- a/src/writerai/types/file_upload_params.py +++ b/src/writerai/types/file_upload_params.py @@ -4,12 +4,13 @@ from typing_extensions import Required, Annotated, TypedDict +from .._types import FileTypes from .._utils import PropertyInfo __all__ = ["FileUploadParams"] class FileUploadParams(TypedDict, total=False): - content: Required[object] + content: Required[FileTypes] content_disposition: Required[Annotated[str, PropertyInfo(alias="Content-Disposition")]] diff --git a/tests/api_resources/test_files.py b/tests/api_resources/test_files.py index 90cb5c04..e0bf2ce8 100644 --- a/tests/api_resources/test_files.py +++ b/tests/api_resources/test_files.py @@ -232,7 +232,7 @@ def test_streaming_response_retry(self, client: Writer) -> None: @parametrize def test_method_upload(self, client: Writer) -> None: file = client.files.upload( - content={}, + content=b"raw file contents", content_disposition="Content-Disposition", ) assert_matches_type(File, file, path=["response"]) @@ -241,7 +241,7 @@ def test_method_upload(self, client: Writer) -> None: @parametrize def test_raw_response_upload(self, client: Writer) -> None: response = client.files.with_raw_response.upload( - content={}, + content=b"raw file contents", content_disposition="Content-Disposition", ) @@ -254,7 +254,7 @@ def test_raw_response_upload(self, client: Writer) -> None: @parametrize def test_streaming_response_upload(self, client: Writer) -> None: with client.files.with_streaming_response.upload( - content={}, + content=b"raw file contents", content_disposition="Content-Disposition", ) as response: assert not response.is_closed @@ -471,7 +471,7 @@ async def test_streaming_response_retry(self, async_client: AsyncWriter) -> None @parametrize async def test_method_upload(self, async_client: AsyncWriter) -> None: file = await async_client.files.upload( - content={}, + content=b"raw file contents", content_disposition="Content-Disposition", ) assert_matches_type(File, file, path=["response"]) @@ -480,7 +480,7 @@ async def test_method_upload(self, async_client: AsyncWriter) -> None: @parametrize async def test_raw_response_upload(self, async_client: AsyncWriter) -> None: response = await async_client.files.with_raw_response.upload( - content={}, + content=b"raw file contents", content_disposition="Content-Disposition", ) @@ -493,7 +493,7 @@ async def test_raw_response_upload(self, async_client: AsyncWriter) -> None: @parametrize async def test_streaming_response_upload(self, async_client: AsyncWriter) -> None: async with async_client.files.with_streaming_response.upload( - content={}, + content=b"raw file contents", content_disposition="Content-Disposition", ) as response: assert not response.is_closed From 4629fc5ab40a2c9b0094defc26d04fb46b9d22fc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 20:09:01 +0000 Subject: [PATCH 143/399] chore(internal): version bump (#143) --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index fbd9082d..7deae338 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.5.0" + ".": "1.6.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 50e88726..d4d7f6d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "1.5.0" +version = "1.6.0" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index 20200b4c..9d2c5455 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "1.5.0" # x-release-please-version +__version__ = "1.6.0" # x-release-please-version From e6102182bd81cfd49f159ed4888068bc58ddda0d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Dec 2024 17:25:28 +0000 Subject: [PATCH 144/399] chore(internal): fix some typos (#145) --- tests/test_client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index c1b16c8b..b7d3bba4 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -354,11 +354,11 @@ def test_default_query_option(self) -> None: FinalRequestOptions( method="get", url="/foo", - params={"foo": "baz", "query_param": "overriden"}, + params={"foo": "baz", "query_param": "overridden"}, ) ) url = httpx.URL(request.url) - assert dict(url.params) == {"foo": "baz", "query_param": "overriden"} + assert dict(url.params) == {"foo": "baz", "query_param": "overridden"} def test_request_extra_json(self) -> None: request = self.client._build_request( @@ -1131,11 +1131,11 @@ def test_default_query_option(self) -> None: FinalRequestOptions( method="get", url="/foo", - params={"foo": "baz", "query_param": "overriden"}, + params={"foo": "baz", "query_param": "overridden"}, ) ) url = httpx.URL(request.url) - assert dict(url.params) == {"foo": "baz", "query_param": "overriden"} + assert dict(url.params) == {"foo": "baz", "query_param": "overridden"} def test_request_extra_json(self) -> None: request = self.client._build_request( From e7c8c7432f63f9a02a65dd0a877b0b8cb979ed7b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Dec 2024 21:19:39 +0000 Subject: [PATCH 145/399] chore(internal): codegen related update (#146) --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 7deae338..59565e8e 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.6.0" + ".": "1.6.1" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index d4d7f6d7..12d24ecc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "1.6.0" +version = "1.6.1" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index 9d2c5455..3f139641 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "1.6.0" # x-release-please-version +__version__ = "1.6.1" # x-release-please-version From ef01bab669ab93b0edee3e5bd98aa30241b60084 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 1 Jan 2025 19:35:31 +0000 Subject: [PATCH 146/399] chore(internal): codegen related update (#147) --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 8dbbf687..38b16626 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2024 Writer + Copyright 2025 Writer Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 255e0aec8c88bf03b8742cb0182a62e2b2cc68fc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 17:13:04 +0000 Subject: [PATCH 147/399] chore: add missing isclass check (#149) --- src/writerai/_models.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/writerai/_models.py b/src/writerai/_models.py index 7a547ce5..d56ea1d9 100644 --- a/src/writerai/_models.py +++ b/src/writerai/_models.py @@ -488,7 +488,11 @@ def construct_type(*, value: object, type_: object) -> object: _, items_type = get_args(type_) # Dict[_, items_type] return {key: construct_type(value=item, type_=items_type) for key, item in value.items()} - if not is_literal_type(type_) and (issubclass(origin, BaseModel) or issubclass(origin, GenericModel)): + if ( + not is_literal_type(type_) + and inspect.isclass(origin) + and (issubclass(origin, BaseModel) or issubclass(origin, GenericModel)) + ): if is_list(value): return [cast(Any, type_).construct(**entry) if is_mapping(entry) else entry for entry in value] From c974a08dde32aa4780edf0c2e01c4f55fc235ada Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Jan 2025 09:34:59 +0000 Subject: [PATCH 148/399] chore(internal): bump httpx dependency (#151) --- pyproject.toml | 2 +- requirements-dev.lock | 5 ++--- requirements.lock | 3 +-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 12d24ecc..8539e31d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,7 +54,7 @@ dev-dependencies = [ "dirty-equals>=0.6.0", "importlib-metadata>=6.7.0", "rich>=13.7.1", - "nest_asyncio==1.6.0" + "nest_asyncio==1.6.0", ] [tool.rye.scripts] diff --git a/requirements-dev.lock b/requirements-dev.lock index 8609015b..d4303b2a 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -35,7 +35,7 @@ h11==0.14.0 # via httpcore httpcore==1.0.2 # via httpx -httpx==0.25.2 +httpx==0.28.1 # via respx # via writer-sdk idna==3.4 @@ -76,7 +76,7 @@ python-dateutil==2.8.2 # via time-machine pytz==2023.3.post1 # via dirty-equals -respx==0.20.2 +respx==0.22.0 rich==13.7.1 ruff==0.6.9 setuptools==68.2.2 @@ -85,7 +85,6 @@ six==1.16.0 # via python-dateutil sniffio==1.3.0 # via anyio - # via httpx # via writer-sdk time-machine==2.9.0 tomli==2.0.2 diff --git a/requirements.lock b/requirements.lock index 4ddaea3d..aae2f01e 100644 --- a/requirements.lock +++ b/requirements.lock @@ -25,7 +25,7 @@ h11==0.14.0 # via httpcore httpcore==1.0.2 # via httpx -httpx==0.25.2 +httpx==0.28.1 # via writer-sdk idna==3.4 # via anyio @@ -36,7 +36,6 @@ pydantic-core==2.27.1 # via pydantic sniffio==1.3.0 # via anyio - # via httpx # via writer-sdk typing-extensions==4.12.2 # via anyio From 8e2647658092d0d4a660907637ab3b45cf1c17be Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Jan 2025 19:58:53 +0000 Subject: [PATCH 149/399] fix(client): only call .close() when needed (#152) --- src/writerai/_base_client.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index 3459f3b6..3f0c30da 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -767,6 +767,9 @@ def __init__(self, **kwargs: Any) -> None: class SyncHttpxClientWrapper(DefaultHttpxClient): def __del__(self) -> None: + if self.is_closed: + return + try: self.close() except Exception: @@ -1334,6 +1337,9 @@ def __init__(self, **kwargs: Any) -> None: class AsyncHttpxClientWrapper(DefaultAsyncHttpxClient): def __del__(self) -> None: + if self.is_closed: + return + try: # TODO(someday): support non asyncio runtimes here asyncio.get_running_loop().create_task(self.aclose()) From d1a72006eff0c43aee4959291887a5e1c77edecf Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 8 Jan 2025 13:36:07 +0000 Subject: [PATCH 150/399] docs: fix typos (#153) --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 25bf57e1..0bad6409 100644 --- a/README.md +++ b/README.md @@ -207,7 +207,7 @@ except writerai.APIStatusError as e: print(e.response) ``` -Error codes are as followed: +Error codes are as follows: | Status Code | Error Type | | ----------- | -------------------------- | @@ -352,8 +352,7 @@ If you need to access undocumented endpoints, params, or response properties, th #### Undocumented endpoints To make requests to undocumented endpoints, you can make requests using `client.get`, `client.post`, and other -http verbs. Options on the client will be respected (such as retries) will be respected when making this -request. +http verbs. Options on the client will be respected (such as retries) when making this request. ```py import httpx From 29babfa47d1327d8a57c41700a06dd6dde967669 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 8 Jan 2025 15:02:54 +0000 Subject: [PATCH 151/399] chore(internal): codegen related update (#154) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0bad6409..aa1ad274 100644 --- a/README.md +++ b/README.md @@ -424,7 +424,7 @@ with Writer() as client: This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions: 1. Changes that only affect static types, without breaking runtime behavior. -2. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals)_. +2. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_ 3. Changes that we do not expect to impact the vast majority of users in practice. We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. From e18c23cb11bd209f1baa0648c6a80c2798cd7823 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 9 Jan 2025 17:54:23 +0000 Subject: [PATCH 152/399] feat(api)!: define chat completion models (#157) --- .stats.yml | 2 +- README.md | 8 +- api.md | 33 ++- src/writerai/_streaming.py | 6 + src/writerai/resources/chat.py | 35 +-- src/writerai/resources/completions.py | 18 +- src/writerai/types/__init__.py | 22 +- src/writerai/types/chat.py | 222 ------------------ src/writerai/types/chat_chat_params.py | 125 +--------- src/writerai/types/chat_completion.py | 54 +++++ src/writerai/types/chat_completion_choice.py | 32 +++ src/writerai/types/chat_completion_chunk.py | 222 +----------------- src/writerai/types/chat_completion_message.py | 28 +++ src/writerai/types/chat_completion_usage.py | 27 +++ src/writerai/types/completion.py | 55 +---- ...{streaming_data.py => completion_chunk.py} | 4 +- src/writerai/types/question.py | 21 +- src/writerai/types/shared/__init__.py | 15 ++ src/writerai/types/shared/error_message.py | 15 ++ src/writerai/types/shared/error_object.py | 16 ++ .../types/shared/function_definition.py | 18 ++ src/writerai/types/shared/function_params.py | 8 + src/writerai/types/shared/graph_data.py | 27 +++ src/writerai/types/shared/logprobs.py | 14 ++ src/writerai/types/shared/logprobs_token.py | 25 ++ src/writerai/types/shared/source.py | 14 ++ src/writerai/types/shared/tool_call.py | 23 ++ .../types/shared/tool_call_streaming.py | 23 ++ .../types/shared/tool_choice_json_object.py | 11 + .../types/shared/tool_choice_string.py | 11 + src/writerai/types/shared/tool_param.py | 37 +++ src/writerai/types/shared_params/__init__.py | 10 + .../shared_params/function_definition.py | 19 ++ .../types/shared_params/function_params.py | 10 + .../types/shared_params/graph_data.py | 28 +++ src/writerai/types/shared_params/source.py | 15 ++ src/writerai/types/shared_params/tool_call.py | 23 ++ .../shared_params/tool_choice_json_object.py | 12 + .../types/shared_params/tool_choice_string.py | 11 + .../types/shared_params/tool_param.py | 38 +++ tests/api_resources/test_chat.py | 18 +- 41 files changed, 691 insertions(+), 664 deletions(-) delete mode 100644 src/writerai/types/chat.py create mode 100644 src/writerai/types/chat_completion.py create mode 100644 src/writerai/types/chat_completion_choice.py create mode 100644 src/writerai/types/chat_completion_message.py create mode 100644 src/writerai/types/chat_completion_usage.py rename src/writerai/types/{streaming_data.py => completion_chunk.py} (68%) create mode 100644 src/writerai/types/shared/__init__.py create mode 100644 src/writerai/types/shared/error_message.py create mode 100644 src/writerai/types/shared/error_object.py create mode 100644 src/writerai/types/shared/function_definition.py create mode 100644 src/writerai/types/shared/function_params.py create mode 100644 src/writerai/types/shared/graph_data.py create mode 100644 src/writerai/types/shared/logprobs.py create mode 100644 src/writerai/types/shared/logprobs_token.py create mode 100644 src/writerai/types/shared/source.py create mode 100644 src/writerai/types/shared/tool_call.py create mode 100644 src/writerai/types/shared/tool_call_streaming.py create mode 100644 src/writerai/types/shared/tool_choice_json_object.py create mode 100644 src/writerai/types/shared/tool_choice_string.py create mode 100644 src/writerai/types/shared/tool_param.py create mode 100644 src/writerai/types/shared_params/__init__.py create mode 100644 src/writerai/types/shared_params/function_definition.py create mode 100644 src/writerai/types/shared_params/function_params.py create mode 100644 src/writerai/types/shared_params/graph_data.py create mode 100644 src/writerai/types/shared_params/source.py create mode 100644 src/writerai/types/shared_params/tool_call.py create mode 100644 src/writerai/types/shared_params/tool_choice_json_object.py create mode 100644 src/writerai/types/shared_params/tool_choice_string.py create mode 100644 src/writerai/types/shared_params/tool_param.py diff --git a/.stats.yml b/.stats.yml index dcd7ed21..ded85c07 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-fcd4d82943d0aeefc300520f0ee4684456ef647140f1d6ba9ffcb86278d83d3a.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-efae0fba75d52fb4c68e8f0332de2486bea6777516ef5cc90163a7c504d95194.yml diff --git a/README.md b/README.md index aa1ad274..334a0e2d 100644 --- a/README.md +++ b/README.md @@ -31,11 +31,11 @@ client = Writer( api_key=os.environ.get("WRITER_API_KEY"), # This is the default and can be omitted ) -chat = client.chat.chat( +chat_completion = client.chat.chat( messages=[{"role": "user"}], model="palmyra-x-004", ) -print(chat.id) +print(chat_completion.id) ``` While you can provide an `api_key` keyword argument, @@ -58,11 +58,11 @@ client = AsyncWriter( async def main() -> None: - chat = await client.chat.chat( + chat_completion = await client.chat.chat( messages=[{"role": "user"}], model="palmyra-x-004", ) - print(chat.id) + print(chat_completion.id) asyncio.run(main()) diff --git a/api.md b/api.md index 15740a86..f1a38a40 100644 --- a/api.md +++ b/api.md @@ -1,3 +1,23 @@ +# Shared Types + +```python +from writerai.types import ( + ErrorMessage, + ErrorObject, + FunctionDefinition, + FunctionParams, + GraphData, + Logprobs, + LogprobsToken, + Source, + ToolCall, + ToolCallStreaming, + ToolChoiceJsonObject, + ToolChoiceString, + ToolParam, +) +``` + # Applications Types: @@ -15,19 +35,26 @@ Methods: Types: ```python -from writerai.types import Chat, ChatCompletionChunk +from writerai.types import ( + ChatCompletion, + ChatCompletionChoice, + ChatCompletionChunk, + ChatCompletionMessage, + ChatCompletionParams, + ChatCompletionUsage, +) ``` Methods: -- client.chat.chat(\*\*params) -> Chat +- client.chat.chat(\*\*params) -> ChatCompletion # Completions Types: ```python -from writerai.types import Completion, StreamingData +from writerai.types import Completion, CompletionChunk, CompletionParams ``` Methods: diff --git a/src/writerai/_streaming.py b/src/writerai/_streaming.py index 794ea612..ca4711bc 100644 --- a/src/writerai/_streaming.py +++ b/src/writerai/_streaming.py @@ -55,6 +55,9 @@ def __stream__(self) -> Iterator[_T]: iterator = self._iter_events() for sse in iterator: + if sse.data.startswith("[DONE]"): + break + if sse.event is None: yield process_data(data=sse.json(), cast_to=cast_to, response=response) @@ -135,6 +138,9 @@ async def __stream__(self) -> AsyncIterator[_T]: iterator = self._iter_events() async for sse in iterator: + if sse.data.startswith("[DONE]"): + break + if sse.event is None: yield process_data(data=sse.json(), cast_to=cast_to, response=response) diff --git a/src/writerai/resources/chat.py b/src/writerai/resources/chat.py index 005aabad..7b7de07f 100644 --- a/src/writerai/resources/chat.py +++ b/src/writerai/resources/chat.py @@ -23,9 +23,10 @@ async_to_streamed_response_wrapper, ) from .._streaming import Stream, AsyncStream -from ..types.chat import Chat from .._base_client import make_request_options +from ..types.chat_completion import ChatCompletion from ..types.chat_completion_chunk import ChatCompletionChunk +from ..types.shared_params.tool_param import ToolParam __all__ = ["ChatResource", "AsyncChatResource"] @@ -64,7 +65,7 @@ def chat( stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, + tools: Iterable[ToolParam] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -72,7 +73,7 @@ def chat( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Chat: + ) -> ChatCompletion: """Generate a chat completion based on the provided messages. The response shown @@ -147,7 +148,7 @@ def chat( stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, + tools: Iterable[ToolParam] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -230,7 +231,7 @@ def chat( stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, + tools: Iterable[ToolParam] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -238,7 +239,7 @@ def chat( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Chat | Stream[ChatCompletionChunk]: + ) -> ChatCompletion | Stream[ChatCompletionChunk]: """Generate a chat completion based on the provided messages. The response shown @@ -313,7 +314,7 @@ def chat( stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, + tools: Iterable[ToolParam] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -321,7 +322,7 @@ def chat( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Chat | Stream[ChatCompletionChunk]: + ) -> ChatCompletion | Stream[ChatCompletionChunk]: return self._post( "/v1/chat", body=maybe_transform( @@ -344,7 +345,7 @@ def chat( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Chat, + cast_to=ChatCompletion, stream=stream or False, stream_cls=Stream[ChatCompletionChunk], ) @@ -384,7 +385,7 @@ async def chat( stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, + tools: Iterable[ToolParam] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -392,7 +393,7 @@ async def chat( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Chat: + ) -> ChatCompletion: """Generate a chat completion based on the provided messages. The response shown @@ -467,7 +468,7 @@ async def chat( stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, + tools: Iterable[ToolParam] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -550,7 +551,7 @@ async def chat( stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, + tools: Iterable[ToolParam] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -558,7 +559,7 @@ async def chat( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Chat | AsyncStream[ChatCompletionChunk]: + ) -> ChatCompletion | AsyncStream[ChatCompletionChunk]: """Generate a chat completion based on the provided messages. The response shown @@ -633,7 +634,7 @@ async def chat( stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[chat_chat_params.Tool] | NotGiven = NOT_GIVEN, + tools: Iterable[ToolParam] | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -641,7 +642,7 @@ async def chat( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Chat | AsyncStream[ChatCompletionChunk]: + ) -> ChatCompletion | AsyncStream[ChatCompletionChunk]: return await self._post( "/v1/chat", body=await async_maybe_transform( @@ -664,7 +665,7 @@ async def chat( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Chat, + cast_to=ChatCompletion, stream=stream or False, stream_cls=AsyncStream[ChatCompletionChunk], ) diff --git a/src/writerai/resources/completions.py b/src/writerai/resources/completions.py index 81162da0..b25c80be 100644 --- a/src/writerai/resources/completions.py +++ b/src/writerai/resources/completions.py @@ -25,7 +25,7 @@ from .._streaming import Stream, AsyncStream from .._base_client import make_request_options from ..types.completion import Completion -from ..types.streaming_data import StreamingData +from ..types.completion_chunk import CompletionChunk __all__ = ["CompletionsResource", "AsyncCompletionsResource"] @@ -130,7 +130,7 @@ def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Stream[StreamingData]: + ) -> Stream[CompletionChunk]: """ Text generation @@ -191,7 +191,7 @@ def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Completion | Stream[StreamingData]: + ) -> Completion | Stream[CompletionChunk]: """ Text generation @@ -252,7 +252,7 @@ def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Completion | Stream[StreamingData]: + ) -> Completion | Stream[CompletionChunk]: return self._post( "/v1/completions", body=maybe_transform( @@ -274,7 +274,7 @@ def create( ), cast_to=Completion, stream=stream or False, - stream_cls=Stream[StreamingData], + stream_cls=Stream[CompletionChunk], ) @@ -378,7 +378,7 @@ async def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AsyncStream[StreamingData]: + ) -> AsyncStream[CompletionChunk]: """ Text generation @@ -439,7 +439,7 @@ async def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Completion | AsyncStream[StreamingData]: + ) -> Completion | AsyncStream[CompletionChunk]: """ Text generation @@ -500,7 +500,7 @@ async def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Completion | AsyncStream[StreamingData]: + ) -> Completion | AsyncStream[CompletionChunk]: return await self._post( "/v1/completions", body=await async_maybe_transform( @@ -522,7 +522,7 @@ async def create( ), cast_to=Completion, stream=stream or False, - stream_cls=AsyncStream[StreamingData], + stream_cls=AsyncStream[CompletionChunk], ) diff --git a/src/writerai/types/__init__.py b/src/writerai/types/__init__.py index cdbbd67c..8e782bc2 100644 --- a/src/writerai/types/__init__.py +++ b/src/writerai/types/__init__.py @@ -2,13 +2,28 @@ from __future__ import annotations -from .chat import Chat as Chat from .file import File as File from .graph import Graph as Graph +from .shared import ( + Source as Source, + Logprobs as Logprobs, + ToolCall as ToolCall, + GraphData as GraphData, + ToolParam as ToolParam, + ErrorObject as ErrorObject, + ErrorMessage as ErrorMessage, + LogprobsToken as LogprobsToken, + FunctionParams as FunctionParams, + ToolChoiceString as ToolChoiceString, + ToolCallStreaming as ToolCallStreaming, + FunctionDefinition as FunctionDefinition, + ToolChoiceJsonObject as ToolChoiceJsonObject, +) from .question import Question as Question from .completion import Completion as Completion -from .streaming_data import StreamingData as StreamingData +from .chat_completion import ChatCompletion as ChatCompletion from .chat_chat_params import ChatChatParams as ChatChatParams +from .completion_chunk import CompletionChunk as CompletionChunk from .file_list_params import FileListParams as FileListParams from .file_retry_params import FileRetryParams as FileRetryParams from .graph_list_params import GraphListParams as GraphListParams @@ -19,11 +34,14 @@ from .model_list_response import ModelListResponse as ModelListResponse from .file_delete_response import FileDeleteResponse as FileDeleteResponse from .chat_completion_chunk import ChatCompletionChunk as ChatCompletionChunk +from .chat_completion_usage import ChatCompletionUsage as ChatCompletionUsage from .graph_create_response import GraphCreateResponse as GraphCreateResponse from .graph_delete_response import GraphDeleteResponse as GraphDeleteResponse from .graph_question_params import GraphQuestionParams as GraphQuestionParams from .graph_update_response import GraphUpdateResponse as GraphUpdateResponse from .tool_parse_pdf_params import ToolParsePdfParams as ToolParsePdfParams +from .chat_completion_choice import ChatCompletionChoice as ChatCompletionChoice +from .chat_completion_message import ChatCompletionMessage as ChatCompletionMessage from .question_response_chunk import QuestionResponseChunk as QuestionResponseChunk from .tool_parse_pdf_response import ToolParsePdfResponse as ToolParsePdfResponse from .completion_create_params import CompletionCreateParams as CompletionCreateParams diff --git a/src/writerai/types/chat.py b/src/writerai/types/chat.py deleted file mode 100644 index 48f7fd73..00000000 --- a/src/writerai/types/chat.py +++ /dev/null @@ -1,222 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List, Optional -from typing_extensions import Literal - -from .._models import BaseModel - -__all__ = [ - "Chat", - "Choice", - "ChoiceMessage", - "ChoiceMessageGraphData", - "ChoiceMessageGraphDataSource", - "ChoiceMessageGraphDataSubquery", - "ChoiceMessageGraphDataSubquerySource", - "ChoiceMessageToolCall", - "ChoiceMessageToolCallFunction", - "ChoiceLogprobs", - "ChoiceLogprobsContent", - "ChoiceLogprobsContentTopLogprob", - "ChoiceLogprobsRefusal", - "ChoiceLogprobsRefusalTopLogprob", - "Usage", - "UsageCompletionTokensDetails", - "UsagePromptTokenDetails", -] - - -class ChoiceMessageGraphDataSource(BaseModel): - file_id: str - """The unique identifier of the file.""" - - snippet: str - """A snippet of text from the source file.""" - - -class ChoiceMessageGraphDataSubquerySource(BaseModel): - file_id: str - """The unique identifier of the file.""" - - snippet: str - """A snippet of text from the source file.""" - - -class ChoiceMessageGraphDataSubquery(BaseModel): - answer: str - """The answer to the subquery.""" - - query: str - """The subquery that was asked.""" - - sources: List[ChoiceMessageGraphDataSubquerySource] - - -class ChoiceMessageGraphData(BaseModel): - sources: Optional[List[ChoiceMessageGraphDataSource]] = None - - status: Optional[Literal["processing", "finished"]] = None - - subqueries: Optional[List[ChoiceMessageGraphDataSubquery]] = None - - -class ChoiceMessageToolCallFunction(BaseModel): - arguments: str - - name: str - - -class ChoiceMessageToolCall(BaseModel): - id: str - - function: ChoiceMessageToolCallFunction - - type: str - - index: Optional[int] = None - - -class ChoiceMessage(BaseModel): - content: str - """The text content produced by the model. - - This field contains the actual output generated, reflecting the model's response - to the input query or command. - """ - - refusal: Optional[str] = None - - role: Literal["assistant"] - """Specifies the role associated with the content.""" - - graph_data: Optional[ChoiceMessageGraphData] = None - - tool_calls: Optional[List[ChoiceMessageToolCall]] = None - - -class ChoiceLogprobsContentTopLogprob(BaseModel): - token: str - - logprob: float - - bytes: Optional[List[int]] = None - - -class ChoiceLogprobsContent(BaseModel): - token: str - - logprob: float - - top_logprobs: List[ChoiceLogprobsContentTopLogprob] - - bytes: Optional[List[int]] = None - - -class ChoiceLogprobsRefusalTopLogprob(BaseModel): - token: str - - logprob: float - - bytes: Optional[List[int]] = None - - -class ChoiceLogprobsRefusal(BaseModel): - token: str - - logprob: float - - top_logprobs: List[ChoiceLogprobsRefusalTopLogprob] - - bytes: Optional[List[int]] = None - - -class ChoiceLogprobs(BaseModel): - content: Optional[List[ChoiceLogprobsContent]] = None - - refusal: Optional[List[ChoiceLogprobsRefusal]] = None - - -class Choice(BaseModel): - finish_reason: Literal["stop", "length", "content_filter", "tool_calls"] - """Describes the condition under which the model ceased generating content. - - Common reasons include 'length' (reached the maximum output size), 'stop' - (encountered a stop sequence), 'content_filter' (harmful content filtered out), - or 'tool_calls' (encountered tool calls). - """ - - index: int - """The index of the choice in the list of completions generated by the model.""" - - message: ChoiceMessage - """The chat completion message from the model. - - Note: this field is deprecated for streaming. Use `delta` instead. - """ - - logprobs: Optional[ChoiceLogprobs] = None - """Log probability information for the choice.""" - - -class UsageCompletionTokensDetails(BaseModel): - reasoning_tokens: int - - -class UsagePromptTokenDetails(BaseModel): - cached_tokens: int - - -class Usage(BaseModel): - completion_tokens: int - - prompt_tokens: int - - total_tokens: int - - completion_tokens_details: Optional[UsageCompletionTokensDetails] = None - - prompt_token_details: Optional[UsagePromptTokenDetails] = None - - -class Chat(BaseModel): - id: str - """A globally unique identifier (UUID) for the response generated by the API. - - This ID can be used to reference the specific operation or transaction within - the system for tracking or debugging purposes. - """ - - choices: List[Choice] - """ - An array of objects representing the different outcomes or results produced by - the model based on the input provided. - """ - - created: int - """The Unix timestamp (in seconds) when the response was created. - - This timestamp can be used to verify the timing of the response relative to - other events or operations. - """ - - model: str - """Identifies the specific model used to generate the response.""" - - object: Literal["chat.completion"] - """ - The type of object returned, which is always `chat.completion` for chat - responses. - """ - - service_tier: Optional[str] = None - """The service tier used for processing the request.""" - - system_fingerprint: Optional[str] = None - """A string representing the backend configuration that the model runs with.""" - - usage: Optional[Usage] = None - """Usage information for the chat completion response. - - Please note that at this time Knowledge Graph tool usage is not included in this - object. - """ diff --git a/src/writerai/types/chat_chat_params.py b/src/writerai/types/chat_chat_params.py index 4bf0acd7..9216e4cf 100644 --- a/src/writerai/types/chat_chat_params.py +++ b/src/writerai/types/chat_chat_params.py @@ -2,27 +2,20 @@ from __future__ import annotations -from typing import Dict, List, Union, Iterable, Optional +from typing import List, Union, Iterable, Optional from typing_extensions import Literal, Required, TypeAlias, TypedDict +from .shared_params.tool_call import ToolCall +from .shared_params.graph_data import GraphData +from .shared_params.tool_param import ToolParam +from .shared_params.tool_choice_string import ToolChoiceString +from .shared_params.tool_choice_json_object import ToolChoiceJsonObject + __all__ = [ "ChatChatParamsBase", "Message", - "MessageGraphData", - "MessageGraphDataSource", - "MessageGraphDataSubquery", - "MessageGraphDataSubquerySource", - "MessageToolCall", - "MessageToolCallFunction", "StreamOptions", "ToolChoice", - "ToolChoiceStringToolChoice", - "ToolChoiceJsonObjectToolChoice", - "Tool", - "ToolFunctionTool", - "ToolFunctionToolFunction", - "ToolGraphTool", - "ToolGraphToolFunction", "ChatChatParamsNonStreaming", "ChatChatParamsStreaming", ] @@ -82,7 +75,7 @@ class ChatChatParamsBase(TypedDict, total=False): pass a specific previously defined function. """ - tools: Iterable[Tool] + tools: Iterable[ToolParam] """ An array of tools described to the model using JSON schema that the model can use to generate responses. Passing graph IDs will automatically use the @@ -98,62 +91,12 @@ class ChatChatParamsBase(TypedDict, total=False): """ -class MessageGraphDataSource(TypedDict, total=False): - file_id: Required[str] - """The unique identifier of the file.""" - - snippet: Required[str] - """A snippet of text from the source file.""" - - -class MessageGraphDataSubquerySource(TypedDict, total=False): - file_id: Required[str] - """The unique identifier of the file.""" - - snippet: Required[str] - """A snippet of text from the source file.""" - - -class MessageGraphDataSubquery(TypedDict, total=False): - answer: Required[str] - """The answer to the subquery.""" - - query: Required[str] - """The subquery that was asked.""" - - sources: Required[Iterable[MessageGraphDataSubquerySource]] - - -class MessageGraphData(TypedDict, total=False): - sources: Iterable[MessageGraphDataSource] - - status: Literal["processing", "finished"] - - subqueries: Iterable[MessageGraphDataSubquery] - - -class MessageToolCallFunction(TypedDict, total=False): - arguments: Required[str] - - name: Required[str] - - -class MessageToolCall(TypedDict, total=False): - id: Required[str] - - function: Required[MessageToolCallFunction] - - type: Required[str] - - index: int - - class Message(TypedDict, total=False): role: Required[Literal["user", "assistant", "system", "tool"]] content: Optional[str] - graph_data: Optional[MessageGraphData] + graph_data: Optional[GraphData] name: Optional[str] @@ -161,7 +104,7 @@ class Message(TypedDict, total=False): tool_call_id: Optional[str] - tool_calls: Optional[Iterable[MessageToolCall]] + tool_calls: Optional[Iterable[ToolCall]] class StreamOptions(TypedDict, total=False): @@ -169,53 +112,7 @@ class StreamOptions(TypedDict, total=False): """Indicate whether to include usage information.""" -class ToolChoiceStringToolChoice(TypedDict, total=False): - value: Required[Literal["none", "auto", "required"]] - - -class ToolChoiceJsonObjectToolChoice(TypedDict, total=False): - value: Required[Dict[str, object]] - - -ToolChoice: TypeAlias = Union[ToolChoiceStringToolChoice, ToolChoiceJsonObjectToolChoice] - - -class ToolFunctionToolFunction(TypedDict, total=False): - name: Required[str] - """Name of the function""" - - description: str - """Description of the function""" - - parameters: Dict[str, object] - - -class ToolFunctionTool(TypedDict, total=False): - function: Required[ToolFunctionToolFunction] - - type: Required[Literal["function"]] - """The type of tool.""" - - -class ToolGraphToolFunction(TypedDict, total=False): - graph_ids: Required[List[str]] - """An array of graph IDs to be used in the tool.""" - - subqueries: Required[bool] - """Boolean to indicate whether to include subqueries in the response.""" - - description: str - """A description of the graph content.""" - - -class ToolGraphTool(TypedDict, total=False): - function: Required[ToolGraphToolFunction] - - type: Required[Literal["graph"]] - """The type of tool.""" - - -Tool: TypeAlias = Union[ToolFunctionTool, ToolGraphTool] +ToolChoice: TypeAlias = Union[ToolChoiceString, ToolChoiceJsonObject] class ChatChatParamsNonStreaming(ChatChatParamsBase, total=False): diff --git a/src/writerai/types/chat_completion.py b/src/writerai/types/chat_completion.py new file mode 100644 index 00000000..a7521fff --- /dev/null +++ b/src/writerai/types/chat_completion.py @@ -0,0 +1,54 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import Literal + +from .._models import BaseModel +from .chat_completion_usage import ChatCompletionUsage +from .chat_completion_choice import ChatCompletionChoice + +__all__ = ["ChatCompletion"] + + +class ChatCompletion(BaseModel): + id: str + """A globally unique identifier (UUID) for the response generated by the API. + + This ID can be used to reference the specific operation or transaction within + the system for tracking or debugging purposes. + """ + + choices: List[ChatCompletionChoice] + """ + An array of objects representing the different outcomes or results produced by + the model based on the input provided. + """ + + created: int + """The Unix timestamp (in seconds) when the response was created. + + This timestamp can be used to verify the timing of the response relative to + other events or operations. + """ + + model: str + """Identifies the specific model used to generate the response.""" + + object: Literal["chat.completion"] + """ + The type of object returned, which is always `chat.completion` for chat + responses. + """ + + service_tier: Optional[str] = None + """The service tier used for processing the request.""" + + system_fingerprint: Optional[str] = None + """A string representing the backend configuration that the model runs with.""" + + usage: Optional[ChatCompletionUsage] = None + """Usage information for the chat completion response. + + Please note that at this time Knowledge Graph tool usage is not included in this + object. + """ diff --git a/src/writerai/types/chat_completion_choice.py b/src/writerai/types/chat_completion_choice.py new file mode 100644 index 00000000..f96b5bee --- /dev/null +++ b/src/writerai/types/chat_completion_choice.py @@ -0,0 +1,32 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from .._models import BaseModel +from .shared.logprobs import Logprobs +from .chat_completion_message import ChatCompletionMessage + +__all__ = ["ChatCompletionChoice"] + + +class ChatCompletionChoice(BaseModel): + finish_reason: Literal["stop", "length", "content_filter", "tool_calls"] + """Describes the condition under which the model ceased generating content. + + Common reasons include 'length' (reached the maximum output size), 'stop' + (encountered a stop sequence), 'content_filter' (harmful content filtered out), + or 'tool_calls' (encountered tool calls). + """ + + index: int + """The index of the choice in the list of completions generated by the model.""" + + message: ChatCompletionMessage + """The chat completion message from the model. + + Note: this field is deprecated for streaming. Use `delta` instead. + """ + + logprobs: Optional[Logprobs] = None + """Log probability information for the choice.""" diff --git a/src/writerai/types/chat_completion_chunk.py b/src/writerai/types/chat_completion_chunk.py index 0a6fe71a..681cc663 100644 --- a/src/writerai/types/chat_completion_chunk.py +++ b/src/writerai/types/chat_completion_chunk.py @@ -4,83 +4,13 @@ from typing_extensions import Literal from .._models import BaseModel +from .shared.logprobs import Logprobs +from .shared.graph_data import GraphData +from .chat_completion_usage import ChatCompletionUsage +from .chat_completion_message import ChatCompletionMessage +from .shared.tool_call_streaming import ToolCallStreaming -__all__ = [ - "ChatCompletionChunk", - "Choice", - "ChoiceDelta", - "ChoiceDeltaGraphData", - "ChoiceDeltaGraphDataSource", - "ChoiceDeltaGraphDataSubquery", - "ChoiceDeltaGraphDataSubquerySource", - "ChoiceDeltaToolCall", - "ChoiceDeltaToolCallFunction", - "ChoiceLogprobs", - "ChoiceLogprobsContent", - "ChoiceLogprobsContentTopLogprob", - "ChoiceLogprobsRefusal", - "ChoiceLogprobsRefusalTopLogprob", - "ChoiceMessage", - "ChoiceMessageGraphData", - "ChoiceMessageGraphDataSource", - "ChoiceMessageGraphDataSubquery", - "ChoiceMessageGraphDataSubquerySource", - "ChoiceMessageToolCall", - "ChoiceMessageToolCallFunction", - "Usage", - "UsageCompletionTokensDetails", - "UsagePromptTokenDetails", -] - - -class ChoiceDeltaGraphDataSource(BaseModel): - file_id: str - """The unique identifier of the file.""" - - snippet: str - """A snippet of text from the source file.""" - - -class ChoiceDeltaGraphDataSubquerySource(BaseModel): - file_id: str - """The unique identifier of the file.""" - - snippet: str - """A snippet of text from the source file.""" - - -class ChoiceDeltaGraphDataSubquery(BaseModel): - answer: str - """The answer to the subquery.""" - - query: str - """The subquery that was asked.""" - - sources: List[ChoiceDeltaGraphDataSubquerySource] - - -class ChoiceDeltaGraphData(BaseModel): - sources: Optional[List[ChoiceDeltaGraphDataSource]] = None - - status: Optional[Literal["processing", "finished"]] = None - - subqueries: Optional[List[ChoiceDeltaGraphDataSubquery]] = None - - -class ChoiceDeltaToolCallFunction(BaseModel): - arguments: str - - name: str - - -class ChoiceDeltaToolCall(BaseModel): - index: int - - id: Optional[str] = None - - function: Optional[ChoiceDeltaToolCallFunction] = None - - type: Optional[str] = None +__all__ = ["ChatCompletionChunk", "Choice", "ChoiceDelta"] class ChoiceDelta(BaseModel): @@ -91,7 +21,7 @@ class ChoiceDelta(BaseModel): to the input query or command. """ - graph_data: Optional[ChoiceDeltaGraphData] = None + graph_data: Optional[GraphData] = None refusal: Optional[str] = None @@ -102,117 +32,7 @@ class ChoiceDelta(BaseModel): output within the interaction flow. """ - tool_calls: Optional[List[ChoiceDeltaToolCall]] = None - - -class ChoiceLogprobsContentTopLogprob(BaseModel): - token: str - - logprob: float - - bytes: Optional[List[int]] = None - - -class ChoiceLogprobsContent(BaseModel): - token: str - - logprob: float - - top_logprobs: List[ChoiceLogprobsContentTopLogprob] - - bytes: Optional[List[int]] = None - - -class ChoiceLogprobsRefusalTopLogprob(BaseModel): - token: str - - logprob: float - - bytes: Optional[List[int]] = None - - -class ChoiceLogprobsRefusal(BaseModel): - token: str - - logprob: float - - top_logprobs: List[ChoiceLogprobsRefusalTopLogprob] - - bytes: Optional[List[int]] = None - - -class ChoiceLogprobs(BaseModel): - content: Optional[List[ChoiceLogprobsContent]] = None - - refusal: Optional[List[ChoiceLogprobsRefusal]] = None - - -class ChoiceMessageGraphDataSource(BaseModel): - file_id: str - """The unique identifier of the file.""" - - snippet: str - """A snippet of text from the source file.""" - - -class ChoiceMessageGraphDataSubquerySource(BaseModel): - file_id: str - """The unique identifier of the file.""" - - snippet: str - """A snippet of text from the source file.""" - - -class ChoiceMessageGraphDataSubquery(BaseModel): - answer: str - """The answer to the subquery.""" - - query: str - """The subquery that was asked.""" - - sources: List[ChoiceMessageGraphDataSubquerySource] - - -class ChoiceMessageGraphData(BaseModel): - sources: Optional[List[ChoiceMessageGraphDataSource]] = None - - status: Optional[Literal["processing", "finished"]] = None - - subqueries: Optional[List[ChoiceMessageGraphDataSubquery]] = None - - -class ChoiceMessageToolCallFunction(BaseModel): - arguments: str - - name: str - - -class ChoiceMessageToolCall(BaseModel): - id: str - - function: ChoiceMessageToolCallFunction - - type: str - - index: Optional[int] = None - - -class ChoiceMessage(BaseModel): - content: str - """The text content produced by the model. - - This field contains the actual output generated, reflecting the model's response - to the input query or command. - """ - - refusal: Optional[str] = None - - role: Literal["assistant"] - """Specifies the role associated with the content.""" - - graph_data: Optional[ChoiceMessageGraphData] = None - - tool_calls: Optional[List[ChoiceMessageToolCall]] = None + tool_calls: Optional[List[ToolCallStreaming]] = None class Choice(BaseModel): @@ -230,36 +50,16 @@ class Choice(BaseModel): index: int """The index of the choice in the list of completions generated by the model.""" - logprobs: Optional[ChoiceLogprobs] = None + logprobs: Optional[Logprobs] = None """Log probability information for the choice.""" - message: Optional[ChoiceMessage] = None + message: Optional[ChatCompletionMessage] = None """The chat completion message from the model. Note: this field is deprecated for streaming. Use `delta` instead. """ -class UsageCompletionTokensDetails(BaseModel): - reasoning_tokens: int - - -class UsagePromptTokenDetails(BaseModel): - cached_tokens: int - - -class Usage(BaseModel): - completion_tokens: int - - prompt_tokens: int - - total_tokens: int - - completion_tokens_details: Optional[UsageCompletionTokensDetails] = None - - prompt_token_details: Optional[UsagePromptTokenDetails] = None - - class ChatCompletionChunk(BaseModel): id: str """A globally unique identifier (UUID) for the response generated by the API. @@ -294,7 +94,7 @@ class ChatCompletionChunk(BaseModel): system_fingerprint: Optional[str] = None - usage: Optional[Usage] = None + usage: Optional[ChatCompletionUsage] = None """Usage information for the chat completion response. Please note that at this time Knowledge Graph tool usage is not included in this diff --git a/src/writerai/types/chat_completion_message.py b/src/writerai/types/chat_completion_message.py new file mode 100644 index 00000000..4187ea36 --- /dev/null +++ b/src/writerai/types/chat_completion_message.py @@ -0,0 +1,28 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import Literal + +from .._models import BaseModel +from .shared.tool_call import ToolCall +from .shared.graph_data import GraphData + +__all__ = ["ChatCompletionMessage"] + + +class ChatCompletionMessage(BaseModel): + content: str + """The text content produced by the model. + + This field contains the actual output generated, reflecting the model's response + to the input query or command. + """ + + refusal: Optional[str] = None + + role: Literal["assistant"] + """Specifies the role associated with the content.""" + + graph_data: Optional[GraphData] = None + + tool_calls: Optional[List[ToolCall]] = None diff --git a/src/writerai/types/chat_completion_usage.py b/src/writerai/types/chat_completion_usage.py new file mode 100644 index 00000000..6fc89018 --- /dev/null +++ b/src/writerai/types/chat_completion_usage.py @@ -0,0 +1,27 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel + +__all__ = ["ChatCompletionUsage", "CompletionTokensDetails", "PromptTokenDetails"] + + +class CompletionTokensDetails(BaseModel): + reasoning_tokens: int + + +class PromptTokenDetails(BaseModel): + cached_tokens: int + + +class ChatCompletionUsage(BaseModel): + completion_tokens: int + + prompt_tokens: int + + total_tokens: int + + completion_tokens_details: Optional[CompletionTokensDetails] = None + + prompt_token_details: Optional[PromptTokenDetails] = None diff --git a/src/writerai/types/completion.py b/src/writerai/types/completion.py index 5f4e82ae..01fe40e5 100644 --- a/src/writerai/types/completion.py +++ b/src/writerai/types/completion.py @@ -3,58 +3,9 @@ from typing import List, Optional from .._models import BaseModel +from .shared.logprobs import Logprobs -__all__ = [ - "Completion", - "Choice", - "ChoiceLogProbs", - "ChoiceLogProbsContent", - "ChoiceLogProbsContentTopLogprob", - "ChoiceLogProbsRefusal", - "ChoiceLogProbsRefusalTopLogprob", -] - - -class ChoiceLogProbsContentTopLogprob(BaseModel): - token: str - - logprob: float - - bytes: Optional[List[int]] = None - - -class ChoiceLogProbsContent(BaseModel): - token: str - - logprob: float - - top_logprobs: List[ChoiceLogProbsContentTopLogprob] - - bytes: Optional[List[int]] = None - - -class ChoiceLogProbsRefusalTopLogprob(BaseModel): - token: str - - logprob: float - - bytes: Optional[List[int]] = None - - -class ChoiceLogProbsRefusal(BaseModel): - token: str - - logprob: float - - top_logprobs: List[ChoiceLogProbsRefusalTopLogprob] - - bytes: Optional[List[int]] = None - - -class ChoiceLogProbs(BaseModel): - content: Optional[List[ChoiceLogProbsContent]] = None - - refusal: Optional[List[ChoiceLogProbsRefusal]] = None +__all__ = ["Completion", "Choice"] class Choice(BaseModel): @@ -64,7 +15,7 @@ class Choice(BaseModel): response. """ - log_probs: Optional[ChoiceLogProbs] = None + log_probs: Optional[Logprobs] = None class Completion(BaseModel): diff --git a/src/writerai/types/streaming_data.py b/src/writerai/types/completion_chunk.py similarity index 68% rename from src/writerai/types/streaming_data.py rename to src/writerai/types/completion_chunk.py index 8c88b456..aec58bd3 100644 --- a/src/writerai/types/streaming_data.py +++ b/src/writerai/types/completion_chunk.py @@ -3,8 +3,8 @@ from .._models import BaseModel -__all__ = ["StreamingData"] +__all__ = ["CompletionChunk"] -class StreamingData(BaseModel): +class CompletionChunk(BaseModel): value: str diff --git a/src/writerai/types/question.py b/src/writerai/types/question.py index 24473a47..4776dfbd 100644 --- a/src/writerai/types/question.py +++ b/src/writerai/types/question.py @@ -3,24 +3,9 @@ from typing import List, Optional from .._models import BaseModel +from .shared.source import Source -__all__ = ["Question", "Source", "Subquery", "SubquerySource"] - - -class Source(BaseModel): - file_id: str - """The unique identifier of the file.""" - - snippet: str - """A snippet of text from the source file.""" - - -class SubquerySource(BaseModel): - file_id: str - """The unique identifier of the file.""" - - snippet: str - """A snippet of text from the source file.""" +__all__ = ["Question", "Subquery"] class Subquery(BaseModel): @@ -30,7 +15,7 @@ class Subquery(BaseModel): query: str """The subquery that was asked.""" - sources: List[SubquerySource] + sources: List[Source] class Question(BaseModel): diff --git a/src/writerai/types/shared/__init__.py b/src/writerai/types/shared/__init__.py new file mode 100644 index 00000000..d62055d0 --- /dev/null +++ b/src/writerai/types/shared/__init__.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .source import Source as Source +from .logprobs import Logprobs as Logprobs +from .tool_call import ToolCall as ToolCall +from .graph_data import GraphData as GraphData +from .tool_param import ToolParam as ToolParam +from .error_object import ErrorObject as ErrorObject +from .error_message import ErrorMessage as ErrorMessage +from .logprobs_token import LogprobsToken as LogprobsToken +from .function_params import FunctionParams as FunctionParams +from .tool_choice_string import ToolChoiceString as ToolChoiceString +from .function_definition import FunctionDefinition as FunctionDefinition +from .tool_call_streaming import ToolCallStreaming as ToolCallStreaming +from .tool_choice_json_object import ToolChoiceJsonObject as ToolChoiceJsonObject diff --git a/src/writerai/types/shared/error_message.py b/src/writerai/types/shared/error_message.py new file mode 100644 index 00000000..2b5b1599 --- /dev/null +++ b/src/writerai/types/shared/error_message.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict + +from ..._models import BaseModel + +__all__ = ["ErrorMessage"] + + +class ErrorMessage(BaseModel): + description: str + + extras: Dict[str, object] + + key: str diff --git a/src/writerai/types/shared/error_object.py b/src/writerai/types/shared/error_object.py new file mode 100644 index 00000000..e98f7a5e --- /dev/null +++ b/src/writerai/types/shared/error_object.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, List + +from ..._models import BaseModel +from .error_message import ErrorMessage + +__all__ = ["ErrorObject"] + + +class ErrorObject(BaseModel): + errors: List[ErrorMessage] + + extras: Dict[str, object] + + tpe: str diff --git a/src/writerai/types/shared/function_definition.py b/src/writerai/types/shared/function_definition.py new file mode 100644 index 00000000..c4f1e678 --- /dev/null +++ b/src/writerai/types/shared/function_definition.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel +from .function_params import FunctionParams + +__all__ = ["FunctionDefinition"] + + +class FunctionDefinition(BaseModel): + name: str + """Name of the function""" + + description: Optional[str] = None + """Description of the function""" + + parameters: Optional[FunctionParams] = None diff --git a/src/writerai/types/shared/function_params.py b/src/writerai/types/shared/function_params.py new file mode 100644 index 00000000..cb00a506 --- /dev/null +++ b/src/writerai/types/shared/function_params.py @@ -0,0 +1,8 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict +from typing_extensions import TypeAlias + +__all__ = ["FunctionParams"] + +FunctionParams: TypeAlias = Dict[str, object] diff --git a/src/writerai/types/shared/graph_data.py b/src/writerai/types/shared/graph_data.py new file mode 100644 index 00000000..b0f21bfb --- /dev/null +++ b/src/writerai/types/shared/graph_data.py @@ -0,0 +1,27 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import Literal + +from .source import Source +from ..._models import BaseModel + +__all__ = ["GraphData", "Subquery"] + + +class Subquery(BaseModel): + answer: str + """The answer to the subquery.""" + + query: str + """The subquery that was asked.""" + + sources: List[Source] + + +class GraphData(BaseModel): + sources: Optional[List[Source]] = None + + status: Optional[Literal["processing", "finished"]] = None + + subqueries: Optional[List[Subquery]] = None diff --git a/src/writerai/types/shared/logprobs.py b/src/writerai/types/shared/logprobs.py new file mode 100644 index 00000000..33dbf7ed --- /dev/null +++ b/src/writerai/types/shared/logprobs.py @@ -0,0 +1,14 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from ..._models import BaseModel +from .logprobs_token import LogprobsToken + +__all__ = ["Logprobs"] + + +class Logprobs(BaseModel): + content: Optional[List[LogprobsToken]] = None + + refusal: Optional[List[LogprobsToken]] = None diff --git a/src/writerai/types/shared/logprobs_token.py b/src/writerai/types/shared/logprobs_token.py new file mode 100644 index 00000000..40c58d67 --- /dev/null +++ b/src/writerai/types/shared/logprobs_token.py @@ -0,0 +1,25 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from ..._models import BaseModel + +__all__ = ["LogprobsToken", "TopLogprob"] + + +class TopLogprob(BaseModel): + token: str + + logprob: float + + bytes: Optional[List[int]] = None + + +class LogprobsToken(BaseModel): + token: str + + logprob: float + + top_logprobs: List[TopLogprob] + + bytes: Optional[List[int]] = None diff --git a/src/writerai/types/shared/source.py b/src/writerai/types/shared/source.py new file mode 100644 index 00000000..f737aa17 --- /dev/null +++ b/src/writerai/types/shared/source.py @@ -0,0 +1,14 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + + +from ..._models import BaseModel + +__all__ = ["Source"] + + +class Source(BaseModel): + file_id: str + """The unique identifier of the file.""" + + snippet: str + """A snippet of text from the source file.""" diff --git a/src/writerai/types/shared/tool_call.py b/src/writerai/types/shared/tool_call.py new file mode 100644 index 00000000..d21d0cc1 --- /dev/null +++ b/src/writerai/types/shared/tool_call.py @@ -0,0 +1,23 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel + +__all__ = ["ToolCall", "Function"] + + +class Function(BaseModel): + arguments: str + + name: Optional[str] = None + + +class ToolCall(BaseModel): + id: str + + function: Function + + type: str + + index: Optional[int] = None diff --git a/src/writerai/types/shared/tool_call_streaming.py b/src/writerai/types/shared/tool_call_streaming.py new file mode 100644 index 00000000..350001f2 --- /dev/null +++ b/src/writerai/types/shared/tool_call_streaming.py @@ -0,0 +1,23 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel + +__all__ = ["ToolCallStreaming", "Function"] + + +class Function(BaseModel): + arguments: str + + name: Optional[str] = None + + +class ToolCallStreaming(BaseModel): + index: int + + id: Optional[str] = None + + function: Optional[Function] = None + + type: Optional[str] = None diff --git a/src/writerai/types/shared/tool_choice_json_object.py b/src/writerai/types/shared/tool_choice_json_object.py new file mode 100644 index 00000000..bc12acf5 --- /dev/null +++ b/src/writerai/types/shared/tool_choice_json_object.py @@ -0,0 +1,11 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict + +from ..._models import BaseModel + +__all__ = ["ToolChoiceJsonObject"] + + +class ToolChoiceJsonObject(BaseModel): + value: Dict[str, object] diff --git a/src/writerai/types/shared/tool_choice_string.py b/src/writerai/types/shared/tool_choice_string.py new file mode 100644 index 00000000..653664da --- /dev/null +++ b/src/writerai/types/shared/tool_choice_string.py @@ -0,0 +1,11 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["ToolChoiceString"] + + +class ToolChoiceString(BaseModel): + value: Literal["none", "auto", "required"] diff --git a/src/writerai/types/shared/tool_param.py b/src/writerai/types/shared/tool_param.py new file mode 100644 index 00000000..93b5ac25 --- /dev/null +++ b/src/writerai/types/shared/tool_param.py @@ -0,0 +1,37 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Union, Optional +from typing_extensions import Literal, TypeAlias + +from ..._models import BaseModel +from .function_definition import FunctionDefinition + +__all__ = ["ToolParam", "FunctionTool", "GraphTool", "GraphToolFunction"] + + +class FunctionTool(BaseModel): + function: FunctionDefinition + + type: Literal["function"] + """The type of tool.""" + + +class GraphToolFunction(BaseModel): + graph_ids: List[str] + """An array of graph IDs to be used in the tool.""" + + subqueries: bool + """Boolean to indicate whether to include subqueries in the response.""" + + description: Optional[str] = None + """A description of the graph content.""" + + +class GraphTool(BaseModel): + function: GraphToolFunction + + type: Literal["graph"] + """The type of tool.""" + + +ToolParam: TypeAlias = Union[FunctionTool, GraphTool] diff --git a/src/writerai/types/shared_params/__init__.py b/src/writerai/types/shared_params/__init__.py new file mode 100644 index 00000000..1bd920cf --- /dev/null +++ b/src/writerai/types/shared_params/__init__.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .source import Source as Source +from .tool_call import ToolCall as ToolCall +from .graph_data import GraphData as GraphData +from .tool_param import ToolParam as ToolParam +from .function_params import FunctionParams as FunctionParams +from .tool_choice_string import ToolChoiceString as ToolChoiceString +from .function_definition import FunctionDefinition as FunctionDefinition +from .tool_choice_json_object import ToolChoiceJsonObject as ToolChoiceJsonObject diff --git a/src/writerai/types/shared_params/function_definition.py b/src/writerai/types/shared_params/function_definition.py new file mode 100644 index 00000000..4043fa70 --- /dev/null +++ b/src/writerai/types/shared_params/function_definition.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +from .function_params import FunctionParams + +__all__ = ["FunctionDefinition"] + + +class FunctionDefinition(TypedDict, total=False): + name: Required[str] + """Name of the function""" + + description: str + """Description of the function""" + + parameters: FunctionParams diff --git a/src/writerai/types/shared_params/function_params.py b/src/writerai/types/shared_params/function_params.py new file mode 100644 index 00000000..dd1f1c2a --- /dev/null +++ b/src/writerai/types/shared_params/function_params.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict +from typing_extensions import TypeAlias + +__all__ = ["FunctionParams"] + +FunctionParams: TypeAlias = Dict[str, object] diff --git a/src/writerai/types/shared_params/graph_data.py b/src/writerai/types/shared_params/graph_data.py new file mode 100644 index 00000000..d89894c0 --- /dev/null +++ b/src/writerai/types/shared_params/graph_data.py @@ -0,0 +1,28 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Iterable +from typing_extensions import Literal, Required, TypedDict + +from .source import Source + +__all__ = ["GraphData", "Subquery"] + + +class Subquery(TypedDict, total=False): + answer: Required[str] + """The answer to the subquery.""" + + query: Required[str] + """The subquery that was asked.""" + + sources: Required[Iterable[Source]] + + +class GraphData(TypedDict, total=False): + sources: Iterable[Source] + + status: Literal["processing", "finished"] + + subqueries: Iterable[Subquery] diff --git a/src/writerai/types/shared_params/source.py b/src/writerai/types/shared_params/source.py new file mode 100644 index 00000000..54b70059 --- /dev/null +++ b/src/writerai/types/shared_params/source.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["Source"] + + +class Source(TypedDict, total=False): + file_id: Required[str] + """The unique identifier of the file.""" + + snippet: Required[str] + """A snippet of text from the source file.""" diff --git a/src/writerai/types/shared_params/tool_call.py b/src/writerai/types/shared_params/tool_call.py new file mode 100644 index 00000000..5a81f596 --- /dev/null +++ b/src/writerai/types/shared_params/tool_call.py @@ -0,0 +1,23 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["ToolCall", "Function"] + + +class Function(TypedDict, total=False): + arguments: Required[str] + + name: str + + +class ToolCall(TypedDict, total=False): + id: Required[str] + + function: Required[Function] + + type: Required[str] + + index: int diff --git a/src/writerai/types/shared_params/tool_choice_json_object.py b/src/writerai/types/shared_params/tool_choice_json_object.py new file mode 100644 index 00000000..4b2cdbdc --- /dev/null +++ b/src/writerai/types/shared_params/tool_choice_json_object.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict +from typing_extensions import Required, TypedDict + +__all__ = ["ToolChoiceJsonObject"] + + +class ToolChoiceJsonObject(TypedDict, total=False): + value: Required[Dict[str, object]] diff --git a/src/writerai/types/shared_params/tool_choice_string.py b/src/writerai/types/shared_params/tool_choice_string.py new file mode 100644 index 00000000..5852b152 --- /dev/null +++ b/src/writerai/types/shared_params/tool_choice_string.py @@ -0,0 +1,11 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["ToolChoiceString"] + + +class ToolChoiceString(TypedDict, total=False): + value: Required[Literal["none", "auto", "required"]] diff --git a/src/writerai/types/shared_params/tool_param.py b/src/writerai/types/shared_params/tool_param.py new file mode 100644 index 00000000..851654c1 --- /dev/null +++ b/src/writerai/types/shared_params/tool_param.py @@ -0,0 +1,38 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Union +from typing_extensions import Literal, Required, TypeAlias, TypedDict + +from .function_definition import FunctionDefinition + +__all__ = ["ToolParam", "FunctionTool", "GraphTool", "GraphToolFunction"] + + +class FunctionTool(TypedDict, total=False): + function: Required[FunctionDefinition] + + type: Required[Literal["function"]] + """The type of tool.""" + + +class GraphToolFunction(TypedDict, total=False): + graph_ids: Required[List[str]] + """An array of graph IDs to be used in the tool.""" + + subqueries: Required[bool] + """Boolean to indicate whether to include subqueries in the response.""" + + description: str + """A description of the graph content.""" + + +class GraphTool(TypedDict, total=False): + function: Required[GraphToolFunction] + + type: Required[Literal["graph"]] + """The type of tool.""" + + +ToolParam: TypeAlias = Union[FunctionTool, GraphTool] diff --git a/tests/api_resources/test_chat.py b/tests/api_resources/test_chat.py index 03c27d4d..f581a9d0 100644 --- a/tests/api_resources/test_chat.py +++ b/tests/api_resources/test_chat.py @@ -9,7 +9,7 @@ from writerai import Writer, AsyncWriter from tests.utils import assert_matches_type -from writerai.types import Chat +from writerai.types import ChatCompletion base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -23,7 +23,7 @@ def test_method_chat_overload_1(self, client: Writer) -> None: messages=[{"role": "user"}], model="palmyra-x-004", ) - assert_matches_type(Chat, chat, path=["response"]) + assert_matches_type(ChatCompletion, chat, path=["response"]) @parametrize def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: @@ -90,7 +90,7 @@ def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: ], top_p=0, ) - assert_matches_type(Chat, chat, path=["response"]) + assert_matches_type(ChatCompletion, chat, path=["response"]) @parametrize def test_raw_response_chat_overload_1(self, client: Writer) -> None: @@ -102,7 +102,7 @@ def test_raw_response_chat_overload_1(self, client: Writer) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" chat = response.parse() - assert_matches_type(Chat, chat, path=["response"]) + assert_matches_type(ChatCompletion, chat, path=["response"]) @parametrize def test_streaming_response_chat_overload_1(self, client: Writer) -> None: @@ -114,7 +114,7 @@ def test_streaming_response_chat_overload_1(self, client: Writer) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" chat = response.parse() - assert_matches_type(Chat, chat, path=["response"]) + assert_matches_type(ChatCompletion, chat, path=["response"]) assert cast(Any, response.is_closed) is True @@ -231,7 +231,7 @@ async def test_method_chat_overload_1(self, async_client: AsyncWriter) -> None: messages=[{"role": "user"}], model="palmyra-x-004", ) - assert_matches_type(Chat, chat, path=["response"]) + assert_matches_type(ChatCompletion, chat, path=["response"]) @parametrize async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncWriter) -> None: @@ -298,7 +298,7 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW ], top_p=0, ) - assert_matches_type(Chat, chat, path=["response"]) + assert_matches_type(ChatCompletion, chat, path=["response"]) @parametrize async def test_raw_response_chat_overload_1(self, async_client: AsyncWriter) -> None: @@ -310,7 +310,7 @@ async def test_raw_response_chat_overload_1(self, async_client: AsyncWriter) -> assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" chat = await response.parse() - assert_matches_type(Chat, chat, path=["response"]) + assert_matches_type(ChatCompletion, chat, path=["response"]) @parametrize async def test_streaming_response_chat_overload_1(self, async_client: AsyncWriter) -> None: @@ -322,7 +322,7 @@ async def test_streaming_response_chat_overload_1(self, async_client: AsyncWrite assert response.http_request.headers.get("X-Stainless-Lang") == "python" chat = await response.parse() - assert_matches_type(Chat, chat, path=["response"]) + assert_matches_type(ChatCompletion, chat, path=["response"]) assert cast(Any, response.is_closed) is True From 1028ac59f794d616a6e9481bbc792f533c3a1bd1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 10 Jan 2025 16:21:25 +0000 Subject: [PATCH 153/399] fix: correctly handle deserialising `cls` fields (#158) --- src/writerai/_models.py | 8 ++++---- tests/test_models.py | 10 ++++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/writerai/_models.py b/src/writerai/_models.py index d56ea1d9..9a918aab 100644 --- a/src/writerai/_models.py +++ b/src/writerai/_models.py @@ -179,14 +179,14 @@ def __str__(self) -> str: @classmethod @override def construct( # pyright: ignore[reportIncompatibleMethodOverride] - cls: Type[ModelT], + __cls: Type[ModelT], _fields_set: set[str] | None = None, **values: object, ) -> ModelT: - m = cls.__new__(cls) + m = __cls.__new__(__cls) fields_values: dict[str, object] = {} - config = get_model_config(cls) + config = get_model_config(__cls) populate_by_name = ( config.allow_population_by_field_name if isinstance(config, _ConfigProtocol) @@ -196,7 +196,7 @@ def construct( # pyright: ignore[reportIncompatibleMethodOverride] if _fields_set is None: _fields_set = set() - model_fields = get_model_fields(cls) + model_fields = get_model_fields(__cls) for name, field in model_fields.items(): key = field.alias if key is None or (key not in values and populate_by_name): diff --git a/tests/test_models.py b/tests/test_models.py index 46788052..d0a41b28 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -844,3 +844,13 @@ class Model(BaseModel): assert m.alias == "foo" assert isinstance(m.union, str) assert m.union == "bar" + + +@pytest.mark.skipif(not PYDANTIC_V2, reason="TypeAliasType is not supported in Pydantic v1") +def test_field_named_cls() -> None: + class Model(BaseModel): + cls: str + + m = construct_type(value={"cls": "foo"}, type_=Model) + assert isinstance(m, Model) + assert isinstance(m.cls, str) From c756b30c8cc5081179a32509f5c526f39259077d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 14 Jan 2025 11:47:41 +0000 Subject: [PATCH 154/399] chore(internal): update deps (#159) --- mypy.ini | 2 +- requirements-dev.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mypy.ini b/mypy.ini index 29be567f..4a26acff 100644 --- a/mypy.ini +++ b/mypy.ini @@ -41,7 +41,7 @@ cache_fine_grained = True # ``` # Changing this codegen to make mypy happy would increase complexity # and would not be worth it. -disable_error_code = func-returns-value +disable_error_code = func-returns-value,overload-cannot-match # https://github.com/python/mypy/issues/12162 [mypy.overrides] diff --git a/requirements-dev.lock b/requirements-dev.lock index d4303b2a..937bd502 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -48,7 +48,7 @@ markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -mypy==1.13.0 +mypy==1.14.1 mypy-extensions==1.0.0 # via mypy nest-asyncio==1.6.0 @@ -68,7 +68,7 @@ pydantic-core==2.27.1 # via pydantic pygments==2.18.0 # via rich -pyright==1.1.390 +pyright==1.1.391 pytest==8.3.3 # via pytest-asyncio pytest-asyncio==0.24.0 From ea0031a05fe6082d32e7afb7001a87fc8559c225 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 14 Jan 2025 19:22:58 +0000 Subject: [PATCH 155/399] docs(api): updates to API spec (#160) --- .stats.yml | 2 +- src/writerai/types/graph.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index ded85c07..7f182d46 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-efae0fba75d52fb4c68e8f0332de2486bea6777516ef5cc90163a7c504d95194.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-95e5c41bc4917566fc6ee1f849795bac2e34e5609afd25bb927252ac7e33e2f0.yml diff --git a/src/writerai/types/graph.py b/src/writerai/types/graph.py index 2d83ddc3..51bd9cc1 100644 --- a/src/writerai/types/graph.py +++ b/src/writerai/types/graph.py @@ -2,6 +2,7 @@ from typing import Optional from datetime import datetime +from typing_extensions import Literal from .._models import BaseModel @@ -34,5 +35,11 @@ class Graph(BaseModel): name: str """The name of the graph.""" + type: Literal["manual", "connector"] + """ + The type of graph, either `manual` (files are uploaded via UI or API) or + `connector` (files are uploaded via a connector). + """ + description: Optional[str] = None """A description of the graph.""" From d52ed4920e0fb124c0a79e93d9159df8df79adce Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 15 Jan 2025 15:43:40 +0000 Subject: [PATCH 156/399] chore(internal): bump pyright dependency (#161) --- requirements-dev.lock | 2 +- src/writerai/_response.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/requirements-dev.lock b/requirements-dev.lock index 937bd502..2c46afe5 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -68,7 +68,7 @@ pydantic-core==2.27.1 # via pydantic pygments==2.18.0 # via rich -pyright==1.1.391 +pyright==1.1.392.post0 pytest==8.3.3 # via pytest-asyncio pytest-asyncio==0.24.0 diff --git a/src/writerai/_response.py b/src/writerai/_response.py index a7f0b0cc..2ed04dfd 100644 --- a/src/writerai/_response.py +++ b/src/writerai/_response.py @@ -210,7 +210,13 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: raise ValueError(f"Subclasses of httpx.Response cannot be passed to `cast_to`") return cast(R, response) - if inspect.isclass(origin) and not issubclass(origin, BaseModel) and issubclass(origin, pydantic.BaseModel): + if ( + inspect.isclass( + origin # pyright: ignore[reportUnknownArgumentType] + ) + and not issubclass(origin, BaseModel) + and issubclass(origin, pydantic.BaseModel) + ): raise TypeError("Pydantic models must subclass our base model type, e.g. `from writerai import BaseModel`") if ( From b2c8e1377cda7f7137d10877caa3b818c7e4783e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 20 Jan 2025 10:08:11 +0000 Subject: [PATCH 157/399] docs(raw responses): fix duplicate `the` (#162) --- src/writerai/resources/applications.py | 4 ++-- src/writerai/resources/chat.py | 4 ++-- src/writerai/resources/completions.py | 4 ++-- src/writerai/resources/files.py | 4 ++-- src/writerai/resources/graphs.py | 4 ++-- src/writerai/resources/models.py | 4 ++-- src/writerai/resources/tools/comprehend.py | 4 ++-- src/writerai/resources/tools/tools.py | 4 ++-- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/writerai/resources/applications.py b/src/writerai/resources/applications.py index 2f8afa3b..63235014 100644 --- a/src/writerai/resources/applications.py +++ b/src/writerai/resources/applications.py @@ -34,7 +34,7 @@ class ApplicationsResource(SyncAPIResource): @cached_property def with_raw_response(self) -> ApplicationsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers @@ -181,7 +181,7 @@ class AsyncApplicationsResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncApplicationsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers diff --git a/src/writerai/resources/chat.py b/src/writerai/resources/chat.py index 7b7de07f..5ba81333 100644 --- a/src/writerai/resources/chat.py +++ b/src/writerai/resources/chat.py @@ -35,7 +35,7 @@ class ChatResource(SyncAPIResource): @cached_property def with_raw_response(self) -> ChatResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers @@ -355,7 +355,7 @@ class AsyncChatResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncChatResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers diff --git a/src/writerai/resources/completions.py b/src/writerai/resources/completions.py index b25c80be..dc8b8251 100644 --- a/src/writerai/resources/completions.py +++ b/src/writerai/resources/completions.py @@ -34,7 +34,7 @@ class CompletionsResource(SyncAPIResource): @cached_property def with_raw_response(self) -> CompletionsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers @@ -282,7 +282,7 @@ class AsyncCompletionsResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncCompletionsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers diff --git a/src/writerai/resources/files.py b/src/writerai/resources/files.py index d4a1cd5c..aec5a5ec 100644 --- a/src/writerai/resources/files.py +++ b/src/writerai/resources/files.py @@ -42,7 +42,7 @@ class FilesResource(SyncAPIResource): @cached_property def with_raw_response(self) -> FilesResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers @@ -300,7 +300,7 @@ class AsyncFilesResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncFilesResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers diff --git a/src/writerai/resources/graphs.py b/src/writerai/resources/graphs.py index ed4e639c..d7194268 100644 --- a/src/writerai/resources/graphs.py +++ b/src/writerai/resources/graphs.py @@ -47,7 +47,7 @@ class GraphsResource(SyncAPIResource): @cached_property def with_raw_response(self) -> GraphsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers @@ -509,7 +509,7 @@ class AsyncGraphsResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncGraphsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers diff --git a/src/writerai/resources/models.py b/src/writerai/resources/models.py index 68410156..6714725e 100644 --- a/src/writerai/resources/models.py +++ b/src/writerai/resources/models.py @@ -23,7 +23,7 @@ class ModelsResource(SyncAPIResource): @cached_property def with_raw_response(self) -> ModelsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers @@ -63,7 +63,7 @@ class AsyncModelsResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncModelsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers diff --git a/src/writerai/resources/tools/comprehend.py b/src/writerai/resources/tools/comprehend.py index b6432171..9d9b8470 100644 --- a/src/writerai/resources/tools/comprehend.py +++ b/src/writerai/resources/tools/comprehend.py @@ -30,7 +30,7 @@ class ComprehendResource(SyncAPIResource): @cached_property def with_raw_response(self) -> ComprehendResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers @@ -97,7 +97,7 @@ class AsyncComprehendResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncComprehendResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers diff --git a/src/writerai/resources/tools/tools.py b/src/writerai/resources/tools/tools.py index 8809cf20..fa6f6414 100644 --- a/src/writerai/resources/tools/tools.py +++ b/src/writerai/resources/tools/tools.py @@ -43,7 +43,7 @@ def comprehend(self) -> ComprehendResource: @cached_property def with_raw_response(self) -> ToolsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers @@ -151,7 +151,7 @@ def comprehend(self) -> AsyncComprehendResource: @cached_property def with_raw_response(self) -> AsyncToolsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers From 0632c9ed3723baec82bd25abd90edce13c60ccce Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 20 Jan 2025 10:32:48 +0000 Subject: [PATCH 158/399] fix(tests): make test_get_platform less flaky (#163) --- tests/test_client.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index b7d3bba4..86a3482f 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -6,6 +6,7 @@ import os import sys import json +import time import asyncio import inspect import subprocess @@ -1651,10 +1652,20 @@ async def test_main() -> None: [sys.executable, "-c", test_code], text=True, ) as process: - try: - process.wait(2) - if process.returncode: - raise AssertionError("calling get_platform using asyncify resulted in a non-zero exit code") - except subprocess.TimeoutExpired as e: - process.kill() - raise AssertionError("calling get_platform using asyncify resulted in a hung process") from e + timeout = 10 # seconds + + start_time = time.monotonic() + while True: + return_code = process.poll() + if return_code is not None: + if return_code != 0: + raise AssertionError("calling get_platform using asyncify resulted in a non-zero exit code") + + # success + break + + if time.monotonic() - start_time > timeout: + process.kill() + raise AssertionError("calling get_platform using asyncify resulted in a hung process") + + time.sleep(0.1) From 72733e3d4bce6f1eec64bcfbcc290e0d2f954698 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 20 Jan 2025 12:09:39 +0000 Subject: [PATCH 159/399] chore(internal): avoid pytest-asyncio deprecation warning (#164) --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 8539e31d..ae8aac05 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -129,6 +129,7 @@ testpaths = ["tests"] addopts = "--tb=short" xfail_strict = true asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "session" filterwarnings = [ "error" ] From ac1b04e10b130967833fea4d6ee38a61416fba9a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 21 Jan 2025 16:03:53 +0000 Subject: [PATCH 160/399] chore(internal): minor style changes (#165) --- src/writerai/_response.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/writerai/_response.py b/src/writerai/_response.py index 2ed04dfd..82dfb2e3 100644 --- a/src/writerai/_response.py +++ b/src/writerai/_response.py @@ -136,6 +136,8 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: if cast_to and is_annotated_type(cast_to): cast_to = extract_type_arg(cast_to, 0) + origin = get_origin(cast_to) or cast_to + if self._is_sse_stream: if to: if not is_stream_class_type(to): @@ -195,8 +197,6 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: if cast_to == bool: return cast(R, response.text.lower() == "true") - origin = get_origin(cast_to) or cast_to - if origin == APIResponse: raise RuntimeError("Unexpected state - cast_to is `APIResponse`") From 1e2862f312f7290cf5a26e5f8cc576c1a9645de3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 12:47:33 +0000 Subject: [PATCH 161/399] chore(internal): minor formatting changes (#166) --- .github/workflows/ci.yml | 3 +-- scripts/bootstrap | 2 +- scripts/lint | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 40293964..c8a8a4f7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,6 @@ jobs: lint: name: lint runs-on: ubuntu-latest - steps: - uses: actions/checkout@v4 @@ -30,6 +29,7 @@ jobs: - name: Run lints run: ./scripts/lint + test: name: test runs-on: ubuntu-latest @@ -50,4 +50,3 @@ jobs: - name: Run tests run: ./scripts/test - diff --git a/scripts/bootstrap b/scripts/bootstrap index 8c5c60eb..e84fe62c 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -4,7 +4,7 @@ set -e cd "$(dirname "$0")/.." -if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ]; then +if ! command -v rye >/dev/null 2>&1 && [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ]; then brew bundle check >/dev/null 2>&1 || { echo "==> Installing Homebrew dependencies…" brew bundle diff --git a/scripts/lint b/scripts/lint index 5ecf173a..a3bd86b1 100755 --- a/scripts/lint +++ b/scripts/lint @@ -9,4 +9,3 @@ rye run lint echo "==> Making sure it imports" rye run python -c 'import writerai' - From 23d75e970aa875f8ad2238799120f0800de2b737 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 16:04:36 +0000 Subject: [PATCH 162/399] feat(api): add async jobs and graph association (#167) --- .stats.yml | 2 +- api.md | 35 +- src/writerai/_client.py | 3 +- src/writerai/pagination.py | 46 +- .../resources/applications/__init__.py | 47 ++ .../{ => applications}/applications.py | 84 +++- src/writerai/resources/applications/graphs.py | 256 ++++++++++ src/writerai/resources/applications/jobs.py | 468 ++++++++++++++++++ src/writerai/types/applications/__init__.py | 12 + .../application_graphs_response.py | 12 + .../types/applications/graph_update_params.py | 13 + .../types/applications/job_create_params.py | 24 + .../types/applications/job_create_response.py | 22 + .../types/applications/job_list_params.py | 41 ++ .../types/applications/job_list_response.py | 25 + .../applications/job_retrieve_response.py | 30 ++ .../types/applications/job_retry_response.py | 22 + tests/api_resources/applications/__init__.py | 1 + .../api_resources/applications/test_graphs.py | 182 +++++++ tests/api_resources/applications/test_jobs.py | 409 +++++++++++++++ 20 files changed, 1720 insertions(+), 14 deletions(-) create mode 100644 src/writerai/resources/applications/__init__.py rename src/writerai/resources/{ => applications}/applications.py (84%) create mode 100644 src/writerai/resources/applications/graphs.py create mode 100644 src/writerai/resources/applications/jobs.py create mode 100644 src/writerai/types/applications/__init__.py create mode 100644 src/writerai/types/applications/application_graphs_response.py create mode 100644 src/writerai/types/applications/graph_update_params.py create mode 100644 src/writerai/types/applications/job_create_params.py create mode 100644 src/writerai/types/applications/job_create_response.py create mode 100644 src/writerai/types/applications/job_list_params.py create mode 100644 src/writerai/types/applications/job_list_response.py create mode 100644 src/writerai/types/applications/job_retrieve_response.py create mode 100644 src/writerai/types/applications/job_retry_response.py create mode 100644 tests/api_resources/applications/__init__.py create mode 100644 tests/api_resources/applications/test_graphs.py create mode 100644 tests/api_resources/applications/test_jobs.py diff --git a/.stats.yml b/.stats.yml index 7f182d46..e1f33816 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ -configured_endpoints: 21 +configured_endpoints: 27 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-95e5c41bc4917566fc6ee1f849795bac2e34e5609afd25bb927252ac7e33e2f0.yml diff --git a/api.md b/api.md index f1a38a40..5fed86c7 100644 --- a/api.md +++ b/api.md @@ -28,7 +28,40 @@ from writerai.types import ApplicationGenerateContentChunk, ApplicationGenerateC Methods: -- client.applications.generate_content(application_id, \*\*params) -> ApplicationGenerateContentResponse +- client.applications.generate_content(application_id, \*\*params) -> ApplicationGenerateContentResponse + +## Jobs + +Types: + +```python +from writerai.types.applications import ( + JobCreateResponse, + JobRetrieveResponse, + JobListResponse, + JobRetryResponse, +) +``` + +Methods: + +- client.applications.jobs.create(application_id, \*\*params) -> JobCreateResponse +- client.applications.jobs.retrieve(job_id) -> JobRetrieveResponse +- client.applications.jobs.list(application_id, \*\*params) -> SyncApplicationJobsOffset[JobListResponse] +- client.applications.jobs.retry(job_id) -> JobRetryResponse + +## Graphs + +Types: + +```python +from writerai.types.applications import ApplicationGraphsResponse +``` + +Methods: + +- client.applications.graphs.update(application_id, \*\*params) -> ApplicationGraphsResponse +- client.applications.graphs.list(application_id) -> ApplicationGraphsResponse # Chat diff --git a/src/writerai/_client.py b/src/writerai/_client.py index 8585f5b9..0a6f5a0f 100644 --- a/src/writerai/_client.py +++ b/src/writerai/_client.py @@ -24,7 +24,7 @@ get_async_library, ) from ._version import __version__ -from .resources import chat, files, graphs, models, completions, applications +from .resources import chat, files, graphs, models, completions from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import WriterError, APIStatusError from ._base_client import ( @@ -33,6 +33,7 @@ AsyncAPIClient, ) from .resources.tools import tools +from .resources.applications import applications __all__ = ["Timeout", "Transport", "ProxiesTypes", "RequestOptions", "Writer", "AsyncWriter", "Client", "AsyncClient"] diff --git a/src/writerai/pagination.py b/src/writerai/pagination.py index 16ba6875..58215e80 100644 --- a/src/writerai/pagination.py +++ b/src/writerai/pagination.py @@ -5,7 +5,7 @@ from ._base_client import BasePage, PageInfo, BaseSyncPage, BaseAsyncPage -__all__ = ["SyncCursorPage", "AsyncCursorPage"] +__all__ = ["SyncCursorPage", "AsyncCursorPage", "SyncApplicationJobsOffset", "AsyncApplicationJobsOffset"] _T = TypeVar("_T") @@ -83,3 +83,47 @@ def next_page_info(self) -> Optional[PageInfo]: return None return PageInfo(params={"before": item.id}) + + +class SyncApplicationJobsOffset(BaseSyncPage[_T], BasePage[_T], Generic[_T]): + jobs: List[_T] + + @override + def _get_page_items(self) -> List[_T]: + jobs = self.jobs + if not jobs: + return [] + return jobs + + @override + def next_page_info(self) -> Optional[PageInfo]: + offset = self._options.params.get("offset") or 0 + if not isinstance(offset, int): + raise ValueError(f'Expected "offset" param to be an integer but got {offset}') + + length = len(self._get_page_items()) + current_count = offset + length + + return PageInfo(params={"offset": current_count}) + + +class AsyncApplicationJobsOffset(BaseAsyncPage[_T], BasePage[_T], Generic[_T]): + jobs: List[_T] + + @override + def _get_page_items(self) -> List[_T]: + jobs = self.jobs + if not jobs: + return [] + return jobs + + @override + def next_page_info(self) -> Optional[PageInfo]: + offset = self._options.params.get("offset") or 0 + if not isinstance(offset, int): + raise ValueError(f'Expected "offset" param to be an integer but got {offset}') + + length = len(self._get_page_items()) + current_count = offset + length + + return PageInfo(params={"offset": current_count}) diff --git a/src/writerai/resources/applications/__init__.py b/src/writerai/resources/applications/__init__.py new file mode 100644 index 00000000..ab99e9c1 --- /dev/null +++ b/src/writerai/resources/applications/__init__.py @@ -0,0 +1,47 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .jobs import ( + JobsResource, + AsyncJobsResource, + JobsResourceWithRawResponse, + AsyncJobsResourceWithRawResponse, + JobsResourceWithStreamingResponse, + AsyncJobsResourceWithStreamingResponse, +) +from .graphs import ( + GraphsResource, + AsyncGraphsResource, + GraphsResourceWithRawResponse, + AsyncGraphsResourceWithRawResponse, + GraphsResourceWithStreamingResponse, + AsyncGraphsResourceWithStreamingResponse, +) +from .applications import ( + ApplicationsResource, + AsyncApplicationsResource, + ApplicationsResourceWithRawResponse, + AsyncApplicationsResourceWithRawResponse, + ApplicationsResourceWithStreamingResponse, + AsyncApplicationsResourceWithStreamingResponse, +) + +__all__ = [ + "JobsResource", + "AsyncJobsResource", + "JobsResourceWithRawResponse", + "AsyncJobsResourceWithRawResponse", + "JobsResourceWithStreamingResponse", + "AsyncJobsResourceWithStreamingResponse", + "GraphsResource", + "AsyncGraphsResource", + "GraphsResourceWithRawResponse", + "AsyncGraphsResourceWithRawResponse", + "GraphsResourceWithStreamingResponse", + "AsyncGraphsResourceWithStreamingResponse", + "ApplicationsResource", + "AsyncApplicationsResource", + "ApplicationsResourceWithRawResponse", + "AsyncApplicationsResourceWithRawResponse", + "ApplicationsResourceWithStreamingResponse", + "AsyncApplicationsResourceWithStreamingResponse", +] diff --git a/src/writerai/resources/applications.py b/src/writerai/resources/applications/applications.py similarity index 84% rename from src/writerai/resources/applications.py rename to src/writerai/resources/applications/applications.py index 63235014..8251b1f5 100644 --- a/src/writerai/resources/applications.py +++ b/src/writerai/resources/applications/applications.py @@ -7,30 +7,54 @@ import httpx -from ..types import application_generate_content_params -from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from .._utils import ( +from .jobs import ( + JobsResource, + AsyncJobsResource, + JobsResourceWithRawResponse, + AsyncJobsResourceWithRawResponse, + JobsResourceWithStreamingResponse, + AsyncJobsResourceWithStreamingResponse, +) +from .graphs import ( + GraphsResource, + AsyncGraphsResource, + GraphsResourceWithRawResponse, + AsyncGraphsResourceWithRawResponse, + GraphsResourceWithStreamingResponse, + AsyncGraphsResourceWithStreamingResponse, +) +from ...types import application_generate_content_params +from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ..._utils import ( required_args, maybe_transform, async_maybe_transform, ) -from .._compat import cached_property -from .._resource import SyncAPIResource, AsyncAPIResource -from .._response import ( +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( to_raw_response_wrapper, to_streamed_response_wrapper, async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from .._streaming import Stream, AsyncStream -from .._base_client import make_request_options -from ..types.application_generate_content_chunk import ApplicationGenerateContentChunk -from ..types.application_generate_content_response import ApplicationGenerateContentResponse +from ..._streaming import Stream, AsyncStream +from ..._base_client import make_request_options +from ...types.application_generate_content_chunk import ApplicationGenerateContentChunk +from ...types.application_generate_content_response import ApplicationGenerateContentResponse __all__ = ["ApplicationsResource", "AsyncApplicationsResource"] class ApplicationsResource(SyncAPIResource): + @cached_property + def jobs(self) -> JobsResource: + return JobsResource(self._client) + + @cached_property + def graphs(self) -> GraphsResource: + return GraphsResource(self._client) + @cached_property def with_raw_response(self) -> ApplicationsResourceWithRawResponse: """ @@ -178,6 +202,14 @@ def generate_content( class AsyncApplicationsResource(AsyncAPIResource): + @cached_property + def jobs(self) -> AsyncJobsResource: + return AsyncJobsResource(self._client) + + @cached_property + def graphs(self) -> AsyncGraphsResource: + return AsyncGraphsResource(self._client) + @cached_property def with_raw_response(self) -> AsyncApplicationsResourceWithRawResponse: """ @@ -332,6 +364,14 @@ def __init__(self, applications: ApplicationsResource) -> None: applications.generate_content, ) + @cached_property + def jobs(self) -> JobsResourceWithRawResponse: + return JobsResourceWithRawResponse(self._applications.jobs) + + @cached_property + def graphs(self) -> GraphsResourceWithRawResponse: + return GraphsResourceWithRawResponse(self._applications.graphs) + class AsyncApplicationsResourceWithRawResponse: def __init__(self, applications: AsyncApplicationsResource) -> None: @@ -341,6 +381,14 @@ def __init__(self, applications: AsyncApplicationsResource) -> None: applications.generate_content, ) + @cached_property + def jobs(self) -> AsyncJobsResourceWithRawResponse: + return AsyncJobsResourceWithRawResponse(self._applications.jobs) + + @cached_property + def graphs(self) -> AsyncGraphsResourceWithRawResponse: + return AsyncGraphsResourceWithRawResponse(self._applications.graphs) + class ApplicationsResourceWithStreamingResponse: def __init__(self, applications: ApplicationsResource) -> None: @@ -350,6 +398,14 @@ def __init__(self, applications: ApplicationsResource) -> None: applications.generate_content, ) + @cached_property + def jobs(self) -> JobsResourceWithStreamingResponse: + return JobsResourceWithStreamingResponse(self._applications.jobs) + + @cached_property + def graphs(self) -> GraphsResourceWithStreamingResponse: + return GraphsResourceWithStreamingResponse(self._applications.graphs) + class AsyncApplicationsResourceWithStreamingResponse: def __init__(self, applications: AsyncApplicationsResource) -> None: @@ -358,3 +414,11 @@ def __init__(self, applications: AsyncApplicationsResource) -> None: self.generate_content = async_to_streamed_response_wrapper( applications.generate_content, ) + + @cached_property + def jobs(self) -> AsyncJobsResourceWithStreamingResponse: + return AsyncJobsResourceWithStreamingResponse(self._applications.jobs) + + @cached_property + def graphs(self) -> AsyncGraphsResourceWithStreamingResponse: + return AsyncGraphsResourceWithStreamingResponse(self._applications.graphs) diff --git a/src/writerai/resources/applications/graphs.py b/src/writerai/resources/applications/graphs.py new file mode 100644 index 00000000..3a739d6e --- /dev/null +++ b/src/writerai/resources/applications/graphs.py @@ -0,0 +1,256 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List + +import httpx + +from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ..._utils import ( + maybe_transform, + async_maybe_transform, +) +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..._base_client import make_request_options +from ...types.applications import graph_update_params +from ...types.applications.application_graphs_response import ApplicationGraphsResponse + +__all__ = ["GraphsResource", "AsyncGraphsResource"] + + +class GraphsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> GraphsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers + """ + return GraphsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> GraphsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/writer/writer-python#with_streaming_response + """ + return GraphsResourceWithStreamingResponse(self) + + def update( + self, + application_id: str, + *, + graph_ids: List[str], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ApplicationGraphsResponse: + """ + Associate graphs with a no-code chat application via API. + + Args: + graph_ids: A list of graph IDs to associate with the application. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not application_id: + raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") + return self._put( + f"/v1/applications/{application_id}/graphs", + body=maybe_transform({"graph_ids": graph_ids}, graph_update_params.GraphUpdateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ApplicationGraphsResponse, + ) + + def list( + self, + application_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ApplicationGraphsResponse: + """ + Retrieve graphs associated with a no-code chat application. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not application_id: + raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") + return self._get( + f"/v1/applications/{application_id}/graphs", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ApplicationGraphsResponse, + ) + + +class AsyncGraphsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncGraphsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers + """ + return AsyncGraphsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncGraphsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/writer/writer-python#with_streaming_response + """ + return AsyncGraphsResourceWithStreamingResponse(self) + + async def update( + self, + application_id: str, + *, + graph_ids: List[str], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ApplicationGraphsResponse: + """ + Associate graphs with a no-code chat application via API. + + Args: + graph_ids: A list of graph IDs to associate with the application. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not application_id: + raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") + return await self._put( + f"/v1/applications/{application_id}/graphs", + body=await async_maybe_transform({"graph_ids": graph_ids}, graph_update_params.GraphUpdateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ApplicationGraphsResponse, + ) + + async def list( + self, + application_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ApplicationGraphsResponse: + """ + Retrieve graphs associated with a no-code chat application. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not application_id: + raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") + return await self._get( + f"/v1/applications/{application_id}/graphs", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ApplicationGraphsResponse, + ) + + +class GraphsResourceWithRawResponse: + def __init__(self, graphs: GraphsResource) -> None: + self._graphs = graphs + + self.update = to_raw_response_wrapper( + graphs.update, + ) + self.list = to_raw_response_wrapper( + graphs.list, + ) + + +class AsyncGraphsResourceWithRawResponse: + def __init__(self, graphs: AsyncGraphsResource) -> None: + self._graphs = graphs + + self.update = async_to_raw_response_wrapper( + graphs.update, + ) + self.list = async_to_raw_response_wrapper( + graphs.list, + ) + + +class GraphsResourceWithStreamingResponse: + def __init__(self, graphs: GraphsResource) -> None: + self._graphs = graphs + + self.update = to_streamed_response_wrapper( + graphs.update, + ) + self.list = to_streamed_response_wrapper( + graphs.list, + ) + + +class AsyncGraphsResourceWithStreamingResponse: + def __init__(self, graphs: AsyncGraphsResource) -> None: + self._graphs = graphs + + self.update = async_to_streamed_response_wrapper( + graphs.update, + ) + self.list = async_to_streamed_response_wrapper( + graphs.list, + ) diff --git a/src/writerai/resources/applications/jobs.py b/src/writerai/resources/applications/jobs.py new file mode 100644 index 00000000..1ef8a01e --- /dev/null +++ b/src/writerai/resources/applications/jobs.py @@ -0,0 +1,468 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict, Iterable + +import httpx + +from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ..._utils import ( + maybe_transform, + async_maybe_transform, +) +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ...pagination import SyncApplicationJobsOffset, AsyncApplicationJobsOffset +from ..._base_client import AsyncPaginator, make_request_options +from ...types.applications import job_list_params, job_create_params +from ...types.applications.job_list_response import JobListResponse +from ...types.applications.job_retry_response import JobRetryResponse +from ...types.applications.job_create_response import JobCreateResponse +from ...types.applications.job_retrieve_response import JobRetrieveResponse + +__all__ = ["JobsResource", "AsyncJobsResource"] + + +class JobsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> JobsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers + """ + return JobsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> JobsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/writer/writer-python#with_streaming_response + """ + return JobsResourceWithStreamingResponse(self) + + def create( + self, + application_id: str, + *, + inputs: Iterable[job_create_params.Input], + metadata: Dict[str, str] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> JobCreateResponse: + """ + Generate content asynchronously from an existing application with inputs. + + Args: + inputs: A list of input objects to generate content for. + + metadata: Optional metadata for the generation request. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not application_id: + raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") + return self._post( + f"/v1/applications/{application_id}/jobs", + body=maybe_transform( + { + "inputs": inputs, + "metadata": metadata, + }, + job_create_params.JobCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=JobCreateResponse, + ) + + def retrieve( + self, + job_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> JobRetrieveResponse: + """ + Retrieves a single job created via the Async API. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not job_id: + raise ValueError(f"Expected a non-empty value for `job_id` but received {job_id!r}") + return self._get( + f"/v1/applications/jobs/{job_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=JobRetrieveResponse, + ) + + def list( + self, + application_id: str, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + status: job_list_params.Status | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncApplicationJobsOffset[JobListResponse]: + """ + Retrieve all jobs created via the async API, linked to the provided application + ID (or alias). + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not application_id: + raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") + return self._get_api_list( + f"/v1/applications/{application_id}/jobs", + page=SyncApplicationJobsOffset[JobListResponse], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "offset": offset, + "status": status, + }, + job_list_params.JobListParams, + ), + ), + model=JobListResponse, + ) + + def retry( + self, + job_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> JobRetryResponse: + """ + Re-triggers the async execution of a single job previously created via the Async + api and terminated in error. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not job_id: + raise ValueError(f"Expected a non-empty value for `job_id` but received {job_id!r}") + return self._post( + f"/v1/applications/jobs/{job_id}/retry", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=JobRetryResponse, + ) + + +class AsyncJobsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncJobsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers + """ + return AsyncJobsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncJobsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/writer/writer-python#with_streaming_response + """ + return AsyncJobsResourceWithStreamingResponse(self) + + async def create( + self, + application_id: str, + *, + inputs: Iterable[job_create_params.Input], + metadata: Dict[str, str] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> JobCreateResponse: + """ + Generate content asynchronously from an existing application with inputs. + + Args: + inputs: A list of input objects to generate content for. + + metadata: Optional metadata for the generation request. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not application_id: + raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") + return await self._post( + f"/v1/applications/{application_id}/jobs", + body=await async_maybe_transform( + { + "inputs": inputs, + "metadata": metadata, + }, + job_create_params.JobCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=JobCreateResponse, + ) + + async def retrieve( + self, + job_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> JobRetrieveResponse: + """ + Retrieves a single job created via the Async API. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not job_id: + raise ValueError(f"Expected a non-empty value for `job_id` but received {job_id!r}") + return await self._get( + f"/v1/applications/jobs/{job_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=JobRetrieveResponse, + ) + + def list( + self, + application_id: str, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + status: job_list_params.Status | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[JobListResponse, AsyncApplicationJobsOffset[JobListResponse]]: + """ + Retrieve all jobs created via the async API, linked to the provided application + ID (or alias). + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not application_id: + raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") + return self._get_api_list( + f"/v1/applications/{application_id}/jobs", + page=AsyncApplicationJobsOffset[JobListResponse], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "offset": offset, + "status": status, + }, + job_list_params.JobListParams, + ), + ), + model=JobListResponse, + ) + + async def retry( + self, + job_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> JobRetryResponse: + """ + Re-triggers the async execution of a single job previously created via the Async + api and terminated in error. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not job_id: + raise ValueError(f"Expected a non-empty value for `job_id` but received {job_id!r}") + return await self._post( + f"/v1/applications/jobs/{job_id}/retry", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=JobRetryResponse, + ) + + +class JobsResourceWithRawResponse: + def __init__(self, jobs: JobsResource) -> None: + self._jobs = jobs + + self.create = to_raw_response_wrapper( + jobs.create, + ) + self.retrieve = to_raw_response_wrapper( + jobs.retrieve, + ) + self.list = to_raw_response_wrapper( + jobs.list, + ) + self.retry = to_raw_response_wrapper( + jobs.retry, + ) + + +class AsyncJobsResourceWithRawResponse: + def __init__(self, jobs: AsyncJobsResource) -> None: + self._jobs = jobs + + self.create = async_to_raw_response_wrapper( + jobs.create, + ) + self.retrieve = async_to_raw_response_wrapper( + jobs.retrieve, + ) + self.list = async_to_raw_response_wrapper( + jobs.list, + ) + self.retry = async_to_raw_response_wrapper( + jobs.retry, + ) + + +class JobsResourceWithStreamingResponse: + def __init__(self, jobs: JobsResource) -> None: + self._jobs = jobs + + self.create = to_streamed_response_wrapper( + jobs.create, + ) + self.retrieve = to_streamed_response_wrapper( + jobs.retrieve, + ) + self.list = to_streamed_response_wrapper( + jobs.list, + ) + self.retry = to_streamed_response_wrapper( + jobs.retry, + ) + + +class AsyncJobsResourceWithStreamingResponse: + def __init__(self, jobs: AsyncJobsResource) -> None: + self._jobs = jobs + + self.create = async_to_streamed_response_wrapper( + jobs.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + jobs.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + jobs.list, + ) + self.retry = async_to_streamed_response_wrapper( + jobs.retry, + ) diff --git a/src/writerai/types/applications/__init__.py b/src/writerai/types/applications/__init__.py new file mode 100644 index 00000000..c0981e1c --- /dev/null +++ b/src/writerai/types/applications/__init__.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .job_list_params import JobListParams as JobListParams +from .job_create_params import JobCreateParams as JobCreateParams +from .job_list_response import JobListResponse as JobListResponse +from .job_retry_response import JobRetryResponse as JobRetryResponse +from .graph_update_params import GraphUpdateParams as GraphUpdateParams +from .job_create_response import JobCreateResponse as JobCreateResponse +from .job_retrieve_response import JobRetrieveResponse as JobRetrieveResponse +from .application_graphs_response import ApplicationGraphsResponse as ApplicationGraphsResponse diff --git a/src/writerai/types/applications/application_graphs_response.py b/src/writerai/types/applications/application_graphs_response.py new file mode 100644 index 00000000..6b87ceb9 --- /dev/null +++ b/src/writerai/types/applications/application_graphs_response.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List + +from ..._models import BaseModel + +__all__ = ["ApplicationGraphsResponse"] + + +class ApplicationGraphsResponse(BaseModel): + graph_ids: List[str] + """A list of graphs associated with the application.""" diff --git a/src/writerai/types/applications/graph_update_params.py b/src/writerai/types/applications/graph_update_params.py new file mode 100644 index 00000000..1e37bf4e --- /dev/null +++ b/src/writerai/types/applications/graph_update_params.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List +from typing_extensions import Required, TypedDict + +__all__ = ["GraphUpdateParams"] + + +class GraphUpdateParams(TypedDict, total=False): + graph_ids: Required[List[str]] + """A list of graph IDs to associate with the application.""" diff --git a/src/writerai/types/applications/job_create_params.py b/src/writerai/types/applications/job_create_params.py new file mode 100644 index 00000000..2cc98aeb --- /dev/null +++ b/src/writerai/types/applications/job_create_params.py @@ -0,0 +1,24 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict, Iterable +from typing_extensions import Required, TypedDict + +__all__ = ["JobCreateParams", "Input"] + + +class JobCreateParams(TypedDict, total=False): + inputs: Required[Iterable[Input]] + """A list of input objects to generate content for.""" + + metadata: Dict[str, str] + """Optional metadata for the generation request.""" + + +class Input(TypedDict, total=False): + content: str + """The input content to be processed.""" + + input_id: str + """A unique identifier for the input object.""" diff --git a/src/writerai/types/applications/job_create_response.py b/src/writerai/types/applications/job_create_response.py new file mode 100644 index 00000000..567785a6 --- /dev/null +++ b/src/writerai/types/applications/job_create_response.py @@ -0,0 +1,22 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime + +from ..._models import BaseModel + +__all__ = ["JobCreateResponse"] + + +class JobCreateResponse(BaseModel): + job_id: str + """The unique identifier for the async job created.""" + + application_id: Optional[str] = None + """The ID of the application associated with this job.""" + + created_at: Optional[datetime] = None + """The timestamp when the job was created.""" + + status: Optional[str] = None + """The initial status of the job (e.g., 'queued').""" diff --git a/src/writerai/types/applications/job_list_params.py b/src/writerai/types/applications/job_list_params.py new file mode 100644 index 00000000..ffdea204 --- /dev/null +++ b/src/writerai/types/applications/job_list_params.py @@ -0,0 +1,41 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union, Iterable +from datetime import datetime +from typing_extensions import Required, Annotated, TypedDict + +from ..._utils import PropertyInfo + +__all__ = ["JobListParams", "Status", "StatusJob"] + + +class JobListParams(TypedDict, total=False): + limit: int + + offset: int + + status: Status + + +class StatusJob(TypedDict, total=False): + created_at: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")] + """The timestamp when the job was created.""" + + job_id: str + """The unique identifier for the job.""" + + result: str + """The result of the completed job, if applicable.""" + + status: str + """The current status of the job.""" + + updated_at: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")] + """The timestamp when the job was last updated.""" + + +class Status(TypedDict, total=False): + jobs: Required[Iterable[StatusJob]] + """A list of jobs associated with the application.""" diff --git a/src/writerai/types/applications/job_list_response.py b/src/writerai/types/applications/job_list_response.py new file mode 100644 index 00000000..463248a4 --- /dev/null +++ b/src/writerai/types/applications/job_list_response.py @@ -0,0 +1,25 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime + +from ..._models import BaseModel + +__all__ = ["JobListResponse"] + + +class JobListResponse(BaseModel): + created_at: Optional[datetime] = None + """The timestamp when the job was created.""" + + job_id: Optional[str] = None + """The unique identifier for the job.""" + + result: Optional[str] = None + """The result of the completed job, if applicable.""" + + status: Optional[str] = None + """The current status of the job.""" + + updated_at: Optional[datetime] = None + """The timestamp when the job was last updated.""" diff --git a/src/writerai/types/applications/job_retrieve_response.py b/src/writerai/types/applications/job_retrieve_response.py new file mode 100644 index 00000000..bd8e0d1f --- /dev/null +++ b/src/writerai/types/applications/job_retrieve_response.py @@ -0,0 +1,30 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from datetime import datetime + +from ..._models import BaseModel + +__all__ = ["JobRetrieveResponse", "Job"] + + +class Job(BaseModel): + created_at: Optional[datetime] = None + """The timestamp when the job was created.""" + + job_id: Optional[str] = None + """The unique identifier for the job.""" + + result: Optional[str] = None + """The result of the completed job, if applicable.""" + + status: Optional[str] = None + """The current status of the job.""" + + updated_at: Optional[datetime] = None + """The timestamp when the job was last updated.""" + + +class JobRetrieveResponse(BaseModel): + jobs: List[Job] + """A list of jobs associated with the application.""" diff --git a/src/writerai/types/applications/job_retry_response.py b/src/writerai/types/applications/job_retry_response.py new file mode 100644 index 00000000..d9b4765a --- /dev/null +++ b/src/writerai/types/applications/job_retry_response.py @@ -0,0 +1,22 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime + +from ..._models import BaseModel + +__all__ = ["JobRetryResponse"] + + +class JobRetryResponse(BaseModel): + job_id: str + """The unique identifier for the async job created.""" + + application_id: Optional[str] = None + """The ID of the application associated with this job.""" + + created_at: Optional[datetime] = None + """The timestamp when the job was created.""" + + status: Optional[str] = None + """The initial status of the job (e.g., 'queued').""" diff --git a/tests/api_resources/applications/__init__.py b/tests/api_resources/applications/__init__.py new file mode 100644 index 00000000..fd8019a9 --- /dev/null +++ b/tests/api_resources/applications/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/applications/test_graphs.py b/tests/api_resources/applications/test_graphs.py new file mode 100644 index 00000000..3228b9d1 --- /dev/null +++ b/tests/api_resources/applications/test_graphs.py @@ -0,0 +1,182 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from writerai import Writer, AsyncWriter +from tests.utils import assert_matches_type +from writerai.types.applications import ApplicationGraphsResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestGraphs: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_update(self, client: Writer) -> None: + graph = client.applications.graphs.update( + application_id="application_id", + graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], + ) + assert_matches_type(ApplicationGraphsResponse, graph, path=["response"]) + + @parametrize + def test_raw_response_update(self, client: Writer) -> None: + response = client.applications.graphs.with_raw_response.update( + application_id="application_id", + graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + graph = response.parse() + assert_matches_type(ApplicationGraphsResponse, graph, path=["response"]) + + @parametrize + def test_streaming_response_update(self, client: Writer) -> None: + with client.applications.graphs.with_streaming_response.update( + application_id="application_id", + graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + graph = response.parse() + assert_matches_type(ApplicationGraphsResponse, graph, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_update(self, client: Writer) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `application_id` but received ''"): + client.applications.graphs.with_raw_response.update( + application_id="", + graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], + ) + + @parametrize + def test_method_list(self, client: Writer) -> None: + graph = client.applications.graphs.list( + "application_id", + ) + assert_matches_type(ApplicationGraphsResponse, graph, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Writer) -> None: + response = client.applications.graphs.with_raw_response.list( + "application_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + graph = response.parse() + assert_matches_type(ApplicationGraphsResponse, graph, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Writer) -> None: + with client.applications.graphs.with_streaming_response.list( + "application_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + graph = response.parse() + assert_matches_type(ApplicationGraphsResponse, graph, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_list(self, client: Writer) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `application_id` but received ''"): + client.applications.graphs.with_raw_response.list( + "", + ) + + +class TestAsyncGraphs: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_update(self, async_client: AsyncWriter) -> None: + graph = await async_client.applications.graphs.update( + application_id="application_id", + graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], + ) + assert_matches_type(ApplicationGraphsResponse, graph, path=["response"]) + + @parametrize + async def test_raw_response_update(self, async_client: AsyncWriter) -> None: + response = await async_client.applications.graphs.with_raw_response.update( + application_id="application_id", + graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + graph = await response.parse() + assert_matches_type(ApplicationGraphsResponse, graph, path=["response"]) + + @parametrize + async def test_streaming_response_update(self, async_client: AsyncWriter) -> None: + async with async_client.applications.graphs.with_streaming_response.update( + application_id="application_id", + graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + graph = await response.parse() + assert_matches_type(ApplicationGraphsResponse, graph, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_update(self, async_client: AsyncWriter) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `application_id` but received ''"): + await async_client.applications.graphs.with_raw_response.update( + application_id="", + graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], + ) + + @parametrize + async def test_method_list(self, async_client: AsyncWriter) -> None: + graph = await async_client.applications.graphs.list( + "application_id", + ) + assert_matches_type(ApplicationGraphsResponse, graph, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncWriter) -> None: + response = await async_client.applications.graphs.with_raw_response.list( + "application_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + graph = await response.parse() + assert_matches_type(ApplicationGraphsResponse, graph, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncWriter) -> None: + async with async_client.applications.graphs.with_streaming_response.list( + "application_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + graph = await response.parse() + assert_matches_type(ApplicationGraphsResponse, graph, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_list(self, async_client: AsyncWriter) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `application_id` but received ''"): + await async_client.applications.graphs.with_raw_response.list( + "", + ) diff --git a/tests/api_resources/applications/test_jobs.py b/tests/api_resources/applications/test_jobs.py new file mode 100644 index 00000000..21e7d8f7 --- /dev/null +++ b/tests/api_resources/applications/test_jobs.py @@ -0,0 +1,409 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from writerai import Writer, AsyncWriter +from tests.utils import assert_matches_type +from writerai._utils import parse_datetime +from writerai.pagination import SyncApplicationJobsOffset, AsyncApplicationJobsOffset +from writerai.types.applications import ( + JobListResponse, + JobRetryResponse, + JobCreateResponse, + JobRetrieveResponse, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestJobs: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: Writer) -> None: + job = client.applications.jobs.create( + application_id="application_id", + inputs=[{}], + ) + assert_matches_type(JobCreateResponse, job, path=["response"]) + + @parametrize + def test_method_create_with_all_params(self, client: Writer) -> None: + job = client.applications.jobs.create( + application_id="application_id", + inputs=[ + { + "content": "content", + "input_id": "input_id", + } + ], + metadata={"foo": "string"}, + ) + assert_matches_type(JobCreateResponse, job, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Writer) -> None: + response = client.applications.jobs.with_raw_response.create( + application_id="application_id", + inputs=[{}], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + job = response.parse() + assert_matches_type(JobCreateResponse, job, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Writer) -> None: + with client.applications.jobs.with_streaming_response.create( + application_id="application_id", + inputs=[{}], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + job = response.parse() + assert_matches_type(JobCreateResponse, job, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_create(self, client: Writer) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `application_id` but received ''"): + client.applications.jobs.with_raw_response.create( + application_id="", + inputs=[{}], + ) + + @parametrize + def test_method_retrieve(self, client: Writer) -> None: + job = client.applications.jobs.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(JobRetrieveResponse, job, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Writer) -> None: + response = client.applications.jobs.with_raw_response.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + job = response.parse() + assert_matches_type(JobRetrieveResponse, job, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Writer) -> None: + with client.applications.jobs.with_streaming_response.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + job = response.parse() + assert_matches_type(JobRetrieveResponse, job, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Writer) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `job_id` but received ''"): + client.applications.jobs.with_raw_response.retrieve( + "", + ) + + @parametrize + def test_method_list(self, client: Writer) -> None: + job = client.applications.jobs.list( + application_id="application_id", + ) + assert_matches_type(SyncApplicationJobsOffset[JobListResponse], job, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Writer) -> None: + job = client.applications.jobs.list( + application_id="application_id", + limit=0, + offset=0, + status={ + "jobs": [ + { + "created_at": parse_datetime("2019-12-27T18:11:19.117Z"), + "job_id": "job_id", + "result": "result", + "status": "status", + "updated_at": parse_datetime("2019-12-27T18:11:19.117Z"), + } + ] + }, + ) + assert_matches_type(SyncApplicationJobsOffset[JobListResponse], job, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Writer) -> None: + response = client.applications.jobs.with_raw_response.list( + application_id="application_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + job = response.parse() + assert_matches_type(SyncApplicationJobsOffset[JobListResponse], job, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Writer) -> None: + with client.applications.jobs.with_streaming_response.list( + application_id="application_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + job = response.parse() + assert_matches_type(SyncApplicationJobsOffset[JobListResponse], job, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_list(self, client: Writer) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `application_id` but received ''"): + client.applications.jobs.with_raw_response.list( + application_id="", + ) + + @parametrize + def test_method_retry(self, client: Writer) -> None: + job = client.applications.jobs.retry( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(JobRetryResponse, job, path=["response"]) + + @parametrize + def test_raw_response_retry(self, client: Writer) -> None: + response = client.applications.jobs.with_raw_response.retry( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + job = response.parse() + assert_matches_type(JobRetryResponse, job, path=["response"]) + + @parametrize + def test_streaming_response_retry(self, client: Writer) -> None: + with client.applications.jobs.with_streaming_response.retry( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + job = response.parse() + assert_matches_type(JobRetryResponse, job, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retry(self, client: Writer) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `job_id` but received ''"): + client.applications.jobs.with_raw_response.retry( + "", + ) + + +class TestAsyncJobs: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_create(self, async_client: AsyncWriter) -> None: + job = await async_client.applications.jobs.create( + application_id="application_id", + inputs=[{}], + ) + assert_matches_type(JobCreateResponse, job, path=["response"]) + + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncWriter) -> None: + job = await async_client.applications.jobs.create( + application_id="application_id", + inputs=[ + { + "content": "content", + "input_id": "input_id", + } + ], + metadata={"foo": "string"}, + ) + assert_matches_type(JobCreateResponse, job, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncWriter) -> None: + response = await async_client.applications.jobs.with_raw_response.create( + application_id="application_id", + inputs=[{}], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + job = await response.parse() + assert_matches_type(JobCreateResponse, job, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncWriter) -> None: + async with async_client.applications.jobs.with_streaming_response.create( + application_id="application_id", + inputs=[{}], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + job = await response.parse() + assert_matches_type(JobCreateResponse, job, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_create(self, async_client: AsyncWriter) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `application_id` but received ''"): + await async_client.applications.jobs.with_raw_response.create( + application_id="", + inputs=[{}], + ) + + @parametrize + async def test_method_retrieve(self, async_client: AsyncWriter) -> None: + job = await async_client.applications.jobs.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(JobRetrieveResponse, job, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncWriter) -> None: + response = await async_client.applications.jobs.with_raw_response.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + job = await response.parse() + assert_matches_type(JobRetrieveResponse, job, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncWriter) -> None: + async with async_client.applications.jobs.with_streaming_response.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + job = await response.parse() + assert_matches_type(JobRetrieveResponse, job, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncWriter) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `job_id` but received ''"): + await async_client.applications.jobs.with_raw_response.retrieve( + "", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncWriter) -> None: + job = await async_client.applications.jobs.list( + application_id="application_id", + ) + assert_matches_type(AsyncApplicationJobsOffset[JobListResponse], job, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncWriter) -> None: + job = await async_client.applications.jobs.list( + application_id="application_id", + limit=0, + offset=0, + status={ + "jobs": [ + { + "created_at": parse_datetime("2019-12-27T18:11:19.117Z"), + "job_id": "job_id", + "result": "result", + "status": "status", + "updated_at": parse_datetime("2019-12-27T18:11:19.117Z"), + } + ] + }, + ) + assert_matches_type(AsyncApplicationJobsOffset[JobListResponse], job, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncWriter) -> None: + response = await async_client.applications.jobs.with_raw_response.list( + application_id="application_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + job = await response.parse() + assert_matches_type(AsyncApplicationJobsOffset[JobListResponse], job, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncWriter) -> None: + async with async_client.applications.jobs.with_streaming_response.list( + application_id="application_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + job = await response.parse() + assert_matches_type(AsyncApplicationJobsOffset[JobListResponse], job, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_list(self, async_client: AsyncWriter) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `application_id` but received ''"): + await async_client.applications.jobs.with_raw_response.list( + application_id="", + ) + + @parametrize + async def test_method_retry(self, async_client: AsyncWriter) -> None: + job = await async_client.applications.jobs.retry( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(JobRetryResponse, job, path=["response"]) + + @parametrize + async def test_raw_response_retry(self, async_client: AsyncWriter) -> None: + response = await async_client.applications.jobs.with_raw_response.retry( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + job = await response.parse() + assert_matches_type(JobRetryResponse, job, path=["response"]) + + @parametrize + async def test_streaming_response_retry(self, async_client: AsyncWriter) -> None: + async with async_client.applications.jobs.with_streaming_response.retry( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + job = await response.parse() + assert_matches_type(JobRetryResponse, job, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retry(self, async_client: AsyncWriter) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `job_id` but received ''"): + await async_client.applications.jobs.with_raw_response.retry( + "", + ) From e59c0cca87a5d5d5d29d70c393622fddf0a19edd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 29 Jan 2025 22:17:47 +0000 Subject: [PATCH 163/399] docs(api): updates to API spec (#168) --- .stats.yml | 2 +- api.md | 2 +- src/writerai/resources/applications/graphs.py | 14 ++- src/writerai/resources/applications/jobs.py | 62 +++++----- .../types/applications/graph_update_params.py | 6 +- .../types/applications/job_create_params.py | 23 ++-- .../types/applications/job_create_response.py | 13 +-- .../types/applications/job_list_params.py | 35 +----- .../types/applications/job_list_response.py | 33 ++++-- .../applications/job_retrieve_response.py | 34 +++--- .../types/applications/job_retry_response.py | 13 +-- tests/api_resources/applications/test_jobs.py | 110 ++++++++---------- 12 files changed, 169 insertions(+), 178 deletions(-) diff --git a/.stats.yml b/.stats.yml index e1f33816..fb5cd384 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 27 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-95e5c41bc4917566fc6ee1f849795bac2e34e5609afd25bb927252ac7e33e2f0.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-6930d4b5bd70658f2379d331f27ccc5ce8898c05abf4dc7bd36ebb2ada18dc7f.yml diff --git a/api.md b/api.md index 5fed86c7..76d757ea 100644 --- a/api.md +++ b/api.md @@ -47,7 +47,7 @@ Methods: - client.applications.jobs.create(application_id, \*\*params) -> JobCreateResponse - client.applications.jobs.retrieve(job_id) -> JobRetrieveResponse -- client.applications.jobs.list(application_id, \*\*params) -> SyncApplicationJobsOffset[JobListResponse] +- client.applications.jobs.list(application_id, \*\*params) -> JobListResponse - client.applications.jobs.retry(job_id) -> JobRetryResponse ## Graphs diff --git a/src/writerai/resources/applications/graphs.py b/src/writerai/resources/applications/graphs.py index 3a739d6e..f393f660 100644 --- a/src/writerai/resources/applications/graphs.py +++ b/src/writerai/resources/applications/graphs.py @@ -59,10 +59,13 @@ def update( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ApplicationGraphsResponse: """ - Associate graphs with a no-code chat application via API. + Updates the graphs listed and associates them with the no-code chat app to be + used. Args: - graph_ids: A list of graph IDs to associate with the application. + graph_ids: A list of graph IDs to associate with the application. Note that this will + replace the existing list of graphs associated with the application, not add to + it. extra_headers: Send extra headers @@ -150,10 +153,13 @@ async def update( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ApplicationGraphsResponse: """ - Associate graphs with a no-code chat application via API. + Updates the graphs listed and associates them with the no-code chat app to be + used. Args: - graph_ids: A list of graph IDs to associate with the application. + graph_ids: A list of graph IDs to associate with the application. Note that this will + replace the existing list of graphs associated with the application, not add to + it. extra_headers: Send extra headers diff --git a/src/writerai/resources/applications/jobs.py b/src/writerai/resources/applications/jobs.py index 1ef8a01e..41d61203 100644 --- a/src/writerai/resources/applications/jobs.py +++ b/src/writerai/resources/applications/jobs.py @@ -2,7 +2,8 @@ from __future__ import annotations -from typing import Dict, Iterable +from typing import Iterable +from typing_extensions import Literal import httpx @@ -19,8 +20,7 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from ...pagination import SyncApplicationJobsOffset, AsyncApplicationJobsOffset -from ..._base_client import AsyncPaginator, make_request_options +from ..._base_client import make_request_options from ...types.applications import job_list_params, job_create_params from ...types.applications.job_list_response import JobListResponse from ...types.applications.job_retry_response import JobRetryResponse @@ -55,7 +55,6 @@ def create( application_id: str, *, inputs: Iterable[job_create_params.Input], - metadata: Dict[str, str] | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -69,8 +68,6 @@ def create( Args: inputs: A list of input objects to generate content for. - metadata: Optional metadata for the generation request. - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -83,13 +80,7 @@ def create( raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") return self._post( f"/v1/applications/{application_id}/jobs", - body=maybe_transform( - { - "inputs": inputs, - "metadata": metadata, - }, - job_create_params.JobCreateParams, - ), + body=maybe_transform({"inputs": inputs}, job_create_params.JobCreateParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -135,19 +126,25 @@ def list( *, limit: int | NotGiven = NOT_GIVEN, offset: int | NotGiven = NOT_GIVEN, - status: job_list_params.Status | NotGiven = NOT_GIVEN, + status: Literal["in_progress", "failed", "completed"] | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SyncApplicationJobsOffset[JobListResponse]: + ) -> JobListResponse: """ Retrieve all jobs created via the async API, linked to the provided application ID (or alias). Args: + limit: The pagination limit for retrieving the jobs. + + offset: The pagination offset for retrieving the jobs. + + status: The status of the job. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -158,9 +155,8 @@ def list( """ if not application_id: raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") - return self._get_api_list( + return self._get( f"/v1/applications/{application_id}/jobs", - page=SyncApplicationJobsOffset[JobListResponse], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -175,7 +171,7 @@ def list( job_list_params.JobListParams, ), ), - model=JobListResponse, + cast_to=JobListResponse, ) def retry( @@ -238,7 +234,6 @@ async def create( application_id: str, *, inputs: Iterable[job_create_params.Input], - metadata: Dict[str, str] | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -252,8 +247,6 @@ async def create( Args: inputs: A list of input objects to generate content for. - metadata: Optional metadata for the generation request. - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -266,13 +259,7 @@ async def create( raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") return await self._post( f"/v1/applications/{application_id}/jobs", - body=await async_maybe_transform( - { - "inputs": inputs, - "metadata": metadata, - }, - job_create_params.JobCreateParams, - ), + body=await async_maybe_transform({"inputs": inputs}, job_create_params.JobCreateParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -312,25 +299,31 @@ async def retrieve( cast_to=JobRetrieveResponse, ) - def list( + async def list( self, application_id: str, *, limit: int | NotGiven = NOT_GIVEN, offset: int | NotGiven = NOT_GIVEN, - status: job_list_params.Status | NotGiven = NOT_GIVEN, + status: Literal["in_progress", "failed", "completed"] | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AsyncPaginator[JobListResponse, AsyncApplicationJobsOffset[JobListResponse]]: + ) -> JobListResponse: """ Retrieve all jobs created via the async API, linked to the provided application ID (or alias). Args: + limit: The pagination limit for retrieving the jobs. + + offset: The pagination offset for retrieving the jobs. + + status: The status of the job. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -341,15 +334,14 @@ def list( """ if not application_id: raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") - return self._get_api_list( + return await self._get( f"/v1/applications/{application_id}/jobs", - page=AsyncApplicationJobsOffset[JobListResponse], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout, - query=maybe_transform( + query=await async_maybe_transform( { "limit": limit, "offset": offset, @@ -358,7 +350,7 @@ def list( job_list_params.JobListParams, ), ), - model=JobListResponse, + cast_to=JobListResponse, ) async def retry( diff --git a/src/writerai/types/applications/graph_update_params.py b/src/writerai/types/applications/graph_update_params.py index 1e37bf4e..ea7be169 100644 --- a/src/writerai/types/applications/graph_update_params.py +++ b/src/writerai/types/applications/graph_update_params.py @@ -10,4 +10,8 @@ class GraphUpdateParams(TypedDict, total=False): graph_ids: Required[List[str]] - """A list of graph IDs to associate with the application.""" + """A list of graph IDs to associate with the application. + + Note that this will replace the existing list of graphs associated with the + application, not add to it. + """ diff --git a/src/writerai/types/applications/job_create_params.py b/src/writerai/types/applications/job_create_params.py index 2cc98aeb..86c6b048 100644 --- a/src/writerai/types/applications/job_create_params.py +++ b/src/writerai/types/applications/job_create_params.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Dict, Iterable +from typing import List, Iterable from typing_extensions import Required, TypedDict __all__ = ["JobCreateParams", "Input"] @@ -12,13 +12,20 @@ class JobCreateParams(TypedDict, total=False): inputs: Required[Iterable[Input]] """A list of input objects to generate content for.""" - metadata: Dict[str, str] - """Optional metadata for the generation request.""" - class Input(TypedDict, total=False): - content: str - """The input content to be processed.""" + id: Required[str] + """The unique identifier for the input field from the application. + + All input types from the No-code application are supported (i.e. Text input, + Dropdown, File upload, Image input). The identifier should be the name of the + input type. + """ + + value: Required[List[str]] + """The value for the input field. - input_id: str - """A unique identifier for the input object.""" + If file is required you will need to pass a `file_id`. See + [here](https://dev.writer.com/api-guides/api-reference/file-api/upload-files) + for the Files API. + """ diff --git a/src/writerai/types/applications/job_create_response.py b/src/writerai/types/applications/job_create_response.py index 567785a6..f83502c2 100644 --- a/src/writerai/types/applications/job_create_response.py +++ b/src/writerai/types/applications/job_create_response.py @@ -1,7 +1,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional from datetime import datetime +from typing_extensions import Literal from ..._models import BaseModel @@ -9,14 +9,11 @@ class JobCreateResponse(BaseModel): - job_id: str + id: str """The unique identifier for the async job created.""" - application_id: Optional[str] = None - """The ID of the application associated with this job.""" - - created_at: Optional[datetime] = None + created_at: datetime """The timestamp when the job was created.""" - status: Optional[str] = None - """The initial status of the job (e.g., 'queued').""" + status: Literal["in_progress", "failed", "completed"] + """The status of the job.""" diff --git a/src/writerai/types/applications/job_list_params.py b/src/writerai/types/applications/job_list_params.py index ffdea204..dfb85b65 100644 --- a/src/writerai/types/applications/job_list_params.py +++ b/src/writerai/types/applications/job_list_params.py @@ -2,40 +2,17 @@ from __future__ import annotations -from typing import Union, Iterable -from datetime import datetime -from typing_extensions import Required, Annotated, TypedDict +from typing_extensions import Literal, TypedDict -from ..._utils import PropertyInfo - -__all__ = ["JobListParams", "Status", "StatusJob"] +__all__ = ["JobListParams"] class JobListParams(TypedDict, total=False): limit: int + """The pagination limit for retrieving the jobs.""" offset: int + """The pagination offset for retrieving the jobs.""" - status: Status - - -class StatusJob(TypedDict, total=False): - created_at: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")] - """The timestamp when the job was created.""" - - job_id: str - """The unique identifier for the job.""" - - result: str - """The result of the completed job, if applicable.""" - - status: str - """The current status of the job.""" - - updated_at: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")] - """The timestamp when the job was last updated.""" - - -class Status(TypedDict, total=False): - jobs: Required[Iterable[StatusJob]] - """A list of jobs associated with the application.""" + status: Literal["in_progress", "failed", "completed"] + """The status of the job.""" diff --git a/src/writerai/types/applications/job_list_response.py b/src/writerai/types/applications/job_list_response.py index 463248a4..dad66049 100644 --- a/src/writerai/types/applications/job_list_response.py +++ b/src/writerai/types/applications/job_list_response.py @@ -1,25 +1,40 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional +from typing import List, Optional from datetime import datetime +from typing_extensions import Literal from ..._models import BaseModel +from ..application_generate_content_response import ApplicationGenerateContentResponse -__all__ = ["JobListResponse"] +__all__ = ["JobListResponse", "Result"] -class JobListResponse(BaseModel): - created_at: Optional[datetime] = None +class Result(BaseModel): + id: str + """The unique identifier for the job.""" + + application_id: str + """The ID of the application associated with this job.""" + + created_at: datetime """The timestamp when the job was created.""" - job_id: Optional[str] = None - """The unique identifier for the job.""" + status: Literal["in_progress", "failed", "completed"] + """The status of the job.""" - result: Optional[str] = None + completed_at: Optional[datetime] = None + """The timestamp when the job was completed.""" + + data: Optional[ApplicationGenerateContentResponse] = None """The result of the completed job, if applicable.""" - status: Optional[str] = None - """The current status of the job.""" + error: Optional[str] = None + """The error message if the job failed.""" updated_at: Optional[datetime] = None """The timestamp when the job was last updated.""" + + +class JobListResponse(BaseModel): + result: Optional[List[Result]] = None diff --git a/src/writerai/types/applications/job_retrieve_response.py b/src/writerai/types/applications/job_retrieve_response.py index bd8e0d1f..8de6ad3e 100644 --- a/src/writerai/types/applications/job_retrieve_response.py +++ b/src/writerai/types/applications/job_retrieve_response.py @@ -1,30 +1,36 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List, Optional +from typing import Optional from datetime import datetime +from typing_extensions import Literal from ..._models import BaseModel +from ..application_generate_content_response import ApplicationGenerateContentResponse -__all__ = ["JobRetrieveResponse", "Job"] +__all__ = ["JobRetrieveResponse"] -class Job(BaseModel): - created_at: Optional[datetime] = None +class JobRetrieveResponse(BaseModel): + id: str + """The unique identifier for the job.""" + + application_id: str + """The ID of the application associated with this job.""" + + created_at: datetime """The timestamp when the job was created.""" - job_id: Optional[str] = None - """The unique identifier for the job.""" + status: Literal["in_progress", "failed", "completed"] + """The status of the job.""" - result: Optional[str] = None + completed_at: Optional[datetime] = None + """The timestamp when the job was completed.""" + + data: Optional[ApplicationGenerateContentResponse] = None """The result of the completed job, if applicable.""" - status: Optional[str] = None - """The current status of the job.""" + error: Optional[str] = None + """The error message if the job failed.""" updated_at: Optional[datetime] = None """The timestamp when the job was last updated.""" - - -class JobRetrieveResponse(BaseModel): - jobs: List[Job] - """A list of jobs associated with the application.""" diff --git a/src/writerai/types/applications/job_retry_response.py b/src/writerai/types/applications/job_retry_response.py index d9b4765a..9555ce6d 100644 --- a/src/writerai/types/applications/job_retry_response.py +++ b/src/writerai/types/applications/job_retry_response.py @@ -1,7 +1,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional from datetime import datetime +from typing_extensions import Literal from ..._models import BaseModel @@ -9,14 +9,11 @@ class JobRetryResponse(BaseModel): - job_id: str + id: str """The unique identifier for the async job created.""" - application_id: Optional[str] = None - """The ID of the application associated with this job.""" - - created_at: Optional[datetime] = None + created_at: datetime """The timestamp when the job was created.""" - status: Optional[str] = None - """The initial status of the job (e.g., 'queued').""" + status: Literal["in_progress", "failed", "completed"] + """The status of the job.""" diff --git a/tests/api_resources/applications/test_jobs.py b/tests/api_resources/applications/test_jobs.py index 21e7d8f7..29dd579d 100644 --- a/tests/api_resources/applications/test_jobs.py +++ b/tests/api_resources/applications/test_jobs.py @@ -9,8 +9,6 @@ from writerai import Writer, AsyncWriter from tests.utils import assert_matches_type -from writerai._utils import parse_datetime -from writerai.pagination import SyncApplicationJobsOffset, AsyncApplicationJobsOffset from writerai.types.applications import ( JobListResponse, JobRetryResponse, @@ -26,23 +24,14 @@ class TestJobs: @parametrize def test_method_create(self, client: Writer) -> None: - job = client.applications.jobs.create( - application_id="application_id", - inputs=[{}], - ) - assert_matches_type(JobCreateResponse, job, path=["response"]) - - @parametrize - def test_method_create_with_all_params(self, client: Writer) -> None: job = client.applications.jobs.create( application_id="application_id", inputs=[ { - "content": "content", - "input_id": "input_id", + "id": "id", + "value": ["string"], } ], - metadata={"foo": "string"}, ) assert_matches_type(JobCreateResponse, job, path=["response"]) @@ -50,7 +39,12 @@ def test_method_create_with_all_params(self, client: Writer) -> None: def test_raw_response_create(self, client: Writer) -> None: response = client.applications.jobs.with_raw_response.create( application_id="application_id", - inputs=[{}], + inputs=[ + { + "id": "id", + "value": ["string"], + } + ], ) assert response.is_closed is True @@ -62,7 +56,12 @@ def test_raw_response_create(self, client: Writer) -> None: def test_streaming_response_create(self, client: Writer) -> None: with client.applications.jobs.with_streaming_response.create( application_id="application_id", - inputs=[{}], + inputs=[ + { + "id": "id", + "value": ["string"], + } + ], ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -77,7 +76,12 @@ def test_path_params_create(self, client: Writer) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `application_id` but received ''"): client.applications.jobs.with_raw_response.create( application_id="", - inputs=[{}], + inputs=[ + { + "id": "id", + "value": ["string"], + } + ], ) @parametrize @@ -123,7 +127,7 @@ def test_method_list(self, client: Writer) -> None: job = client.applications.jobs.list( application_id="application_id", ) - assert_matches_type(SyncApplicationJobsOffset[JobListResponse], job, path=["response"]) + assert_matches_type(JobListResponse, job, path=["response"]) @parametrize def test_method_list_with_all_params(self, client: Writer) -> None: @@ -131,19 +135,9 @@ def test_method_list_with_all_params(self, client: Writer) -> None: application_id="application_id", limit=0, offset=0, - status={ - "jobs": [ - { - "created_at": parse_datetime("2019-12-27T18:11:19.117Z"), - "job_id": "job_id", - "result": "result", - "status": "status", - "updated_at": parse_datetime("2019-12-27T18:11:19.117Z"), - } - ] - }, + status="in_progress", ) - assert_matches_type(SyncApplicationJobsOffset[JobListResponse], job, path=["response"]) + assert_matches_type(JobListResponse, job, path=["response"]) @parametrize def test_raw_response_list(self, client: Writer) -> None: @@ -154,7 +148,7 @@ def test_raw_response_list(self, client: Writer) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" job = response.parse() - assert_matches_type(SyncApplicationJobsOffset[JobListResponse], job, path=["response"]) + assert_matches_type(JobListResponse, job, path=["response"]) @parametrize def test_streaming_response_list(self, client: Writer) -> None: @@ -165,7 +159,7 @@ def test_streaming_response_list(self, client: Writer) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" job = response.parse() - assert_matches_type(SyncApplicationJobsOffset[JobListResponse], job, path=["response"]) + assert_matches_type(JobListResponse, job, path=["response"]) assert cast(Any, response.is_closed) is True @@ -220,23 +214,14 @@ class TestAsyncJobs: @parametrize async def test_method_create(self, async_client: AsyncWriter) -> None: - job = await async_client.applications.jobs.create( - application_id="application_id", - inputs=[{}], - ) - assert_matches_type(JobCreateResponse, job, path=["response"]) - - @parametrize - async def test_method_create_with_all_params(self, async_client: AsyncWriter) -> None: job = await async_client.applications.jobs.create( application_id="application_id", inputs=[ { - "content": "content", - "input_id": "input_id", + "id": "id", + "value": ["string"], } ], - metadata={"foo": "string"}, ) assert_matches_type(JobCreateResponse, job, path=["response"]) @@ -244,7 +229,12 @@ async def test_method_create_with_all_params(self, async_client: AsyncWriter) -> async def test_raw_response_create(self, async_client: AsyncWriter) -> None: response = await async_client.applications.jobs.with_raw_response.create( application_id="application_id", - inputs=[{}], + inputs=[ + { + "id": "id", + "value": ["string"], + } + ], ) assert response.is_closed is True @@ -256,7 +246,12 @@ async def test_raw_response_create(self, async_client: AsyncWriter) -> None: async def test_streaming_response_create(self, async_client: AsyncWriter) -> None: async with async_client.applications.jobs.with_streaming_response.create( application_id="application_id", - inputs=[{}], + inputs=[ + { + "id": "id", + "value": ["string"], + } + ], ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -271,7 +266,12 @@ async def test_path_params_create(self, async_client: AsyncWriter) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `application_id` but received ''"): await async_client.applications.jobs.with_raw_response.create( application_id="", - inputs=[{}], + inputs=[ + { + "id": "id", + "value": ["string"], + } + ], ) @parametrize @@ -317,7 +317,7 @@ async def test_method_list(self, async_client: AsyncWriter) -> None: job = await async_client.applications.jobs.list( application_id="application_id", ) - assert_matches_type(AsyncApplicationJobsOffset[JobListResponse], job, path=["response"]) + assert_matches_type(JobListResponse, job, path=["response"]) @parametrize async def test_method_list_with_all_params(self, async_client: AsyncWriter) -> None: @@ -325,19 +325,9 @@ async def test_method_list_with_all_params(self, async_client: AsyncWriter) -> N application_id="application_id", limit=0, offset=0, - status={ - "jobs": [ - { - "created_at": parse_datetime("2019-12-27T18:11:19.117Z"), - "job_id": "job_id", - "result": "result", - "status": "status", - "updated_at": parse_datetime("2019-12-27T18:11:19.117Z"), - } - ] - }, + status="in_progress", ) - assert_matches_type(AsyncApplicationJobsOffset[JobListResponse], job, path=["response"]) + assert_matches_type(JobListResponse, job, path=["response"]) @parametrize async def test_raw_response_list(self, async_client: AsyncWriter) -> None: @@ -348,7 +338,7 @@ async def test_raw_response_list(self, async_client: AsyncWriter) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" job = await response.parse() - assert_matches_type(AsyncApplicationJobsOffset[JobListResponse], job, path=["response"]) + assert_matches_type(JobListResponse, job, path=["response"]) @parametrize async def test_streaming_response_list(self, async_client: AsyncWriter) -> None: @@ -359,7 +349,7 @@ async def test_streaming_response_list(self, async_client: AsyncWriter) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" job = await response.parse() - assert_matches_type(AsyncApplicationJobsOffset[JobListResponse], job, path=["response"]) + assert_matches_type(JobListResponse, job, path=["response"]) assert cast(Any, response.is_closed) is True From 5ad83ca068e168dddb0eeaf540a542b0de8056ec Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 29 Jan 2025 22:20:01 +0000 Subject: [PATCH 164/399] feat(api): update application jobs pagination response (#169) --- .stats.yml | 2 +- src/writerai/pagination.py | 16 ++-- src/writerai/resources/applications/graphs.py | 14 +-- src/writerai/resources/applications/jobs.py | 41 ++++---- .../types/applications/graph_update_params.py | 6 +- .../types/applications/job_create_params.py | 23 ++--- .../types/applications/job_create_response.py | 13 ++- .../types/applications/job_list_params.py | 35 +++++-- .../types/applications/job_list_response.py | 30 ++---- .../applications/job_retrieve_response.py | 34 +++---- .../types/applications/job_retry_response.py | 13 ++- tests/api_resources/applications/test_jobs.py | 93 ++++++++++--------- 12 files changed, 165 insertions(+), 155 deletions(-) diff --git a/.stats.yml b/.stats.yml index fb5cd384..e1f33816 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 27 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-6930d4b5bd70658f2379d331f27ccc5ce8898c05abf4dc7bd36ebb2ada18dc7f.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-95e5c41bc4917566fc6ee1f849795bac2e34e5609afd25bb927252ac7e33e2f0.yml diff --git a/src/writerai/pagination.py b/src/writerai/pagination.py index 58215e80..6a634195 100644 --- a/src/writerai/pagination.py +++ b/src/writerai/pagination.py @@ -86,14 +86,14 @@ def next_page_info(self) -> Optional[PageInfo]: class SyncApplicationJobsOffset(BaseSyncPage[_T], BasePage[_T], Generic[_T]): - jobs: List[_T] + result: List[_T] @override def _get_page_items(self) -> List[_T]: - jobs = self.jobs - if not jobs: + result = self.result + if not result: return [] - return jobs + return result @override def next_page_info(self) -> Optional[PageInfo]: @@ -108,14 +108,14 @@ def next_page_info(self) -> Optional[PageInfo]: class AsyncApplicationJobsOffset(BaseAsyncPage[_T], BasePage[_T], Generic[_T]): - jobs: List[_T] + result: List[_T] @override def _get_page_items(self) -> List[_T]: - jobs = self.jobs - if not jobs: + result = self.result + if not result: return [] - return jobs + return result @override def next_page_info(self) -> Optional[PageInfo]: diff --git a/src/writerai/resources/applications/graphs.py b/src/writerai/resources/applications/graphs.py index f393f660..3a739d6e 100644 --- a/src/writerai/resources/applications/graphs.py +++ b/src/writerai/resources/applications/graphs.py @@ -59,13 +59,10 @@ def update( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ApplicationGraphsResponse: """ - Updates the graphs listed and associates them with the no-code chat app to be - used. + Associate graphs with a no-code chat application via API. Args: - graph_ids: A list of graph IDs to associate with the application. Note that this will - replace the existing list of graphs associated with the application, not add to - it. + graph_ids: A list of graph IDs to associate with the application. extra_headers: Send extra headers @@ -153,13 +150,10 @@ async def update( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ApplicationGraphsResponse: """ - Updates the graphs listed and associates them with the no-code chat app to be - used. + Associate graphs with a no-code chat application via API. Args: - graph_ids: A list of graph IDs to associate with the application. Note that this will - replace the existing list of graphs associated with the application, not add to - it. + graph_ids: A list of graph IDs to associate with the application. extra_headers: Send extra headers diff --git a/src/writerai/resources/applications/jobs.py b/src/writerai/resources/applications/jobs.py index 41d61203..19f2f3ee 100644 --- a/src/writerai/resources/applications/jobs.py +++ b/src/writerai/resources/applications/jobs.py @@ -2,8 +2,7 @@ from __future__ import annotations -from typing import Iterable -from typing_extensions import Literal +from typing import Dict, Iterable import httpx @@ -55,6 +54,7 @@ def create( application_id: str, *, inputs: Iterable[job_create_params.Input], + metadata: Dict[str, str] | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -68,6 +68,8 @@ def create( Args: inputs: A list of input objects to generate content for. + metadata: Optional metadata for the generation request. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -80,7 +82,13 @@ def create( raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") return self._post( f"/v1/applications/{application_id}/jobs", - body=maybe_transform({"inputs": inputs}, job_create_params.JobCreateParams), + body=maybe_transform( + { + "inputs": inputs, + "metadata": metadata, + }, + job_create_params.JobCreateParams, + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -126,7 +134,7 @@ def list( *, limit: int | NotGiven = NOT_GIVEN, offset: int | NotGiven = NOT_GIVEN, - status: Literal["in_progress", "failed", "completed"] | NotGiven = NOT_GIVEN, + status: job_list_params.Status | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -139,12 +147,6 @@ def list( ID (or alias). Args: - limit: The pagination limit for retrieving the jobs. - - offset: The pagination offset for retrieving the jobs. - - status: The status of the job. - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -234,6 +236,7 @@ async def create( application_id: str, *, inputs: Iterable[job_create_params.Input], + metadata: Dict[str, str] | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -247,6 +250,8 @@ async def create( Args: inputs: A list of input objects to generate content for. + metadata: Optional metadata for the generation request. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -259,7 +264,13 @@ async def create( raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") return await self._post( f"/v1/applications/{application_id}/jobs", - body=await async_maybe_transform({"inputs": inputs}, job_create_params.JobCreateParams), + body=await async_maybe_transform( + { + "inputs": inputs, + "metadata": metadata, + }, + job_create_params.JobCreateParams, + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -305,7 +316,7 @@ async def list( *, limit: int | NotGiven = NOT_GIVEN, offset: int | NotGiven = NOT_GIVEN, - status: Literal["in_progress", "failed", "completed"] | NotGiven = NOT_GIVEN, + status: job_list_params.Status | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -318,12 +329,6 @@ async def list( ID (or alias). Args: - limit: The pagination limit for retrieving the jobs. - - offset: The pagination offset for retrieving the jobs. - - status: The status of the job. - extra_headers: Send extra headers extra_query: Add additional query parameters to the request diff --git a/src/writerai/types/applications/graph_update_params.py b/src/writerai/types/applications/graph_update_params.py index ea7be169..1e37bf4e 100644 --- a/src/writerai/types/applications/graph_update_params.py +++ b/src/writerai/types/applications/graph_update_params.py @@ -10,8 +10,4 @@ class GraphUpdateParams(TypedDict, total=False): graph_ids: Required[List[str]] - """A list of graph IDs to associate with the application. - - Note that this will replace the existing list of graphs associated with the - application, not add to it. - """ + """A list of graph IDs to associate with the application.""" diff --git a/src/writerai/types/applications/job_create_params.py b/src/writerai/types/applications/job_create_params.py index 86c6b048..2cc98aeb 100644 --- a/src/writerai/types/applications/job_create_params.py +++ b/src/writerai/types/applications/job_create_params.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import List, Iterable +from typing import Dict, Iterable from typing_extensions import Required, TypedDict __all__ = ["JobCreateParams", "Input"] @@ -12,20 +12,13 @@ class JobCreateParams(TypedDict, total=False): inputs: Required[Iterable[Input]] """A list of input objects to generate content for.""" + metadata: Dict[str, str] + """Optional metadata for the generation request.""" -class Input(TypedDict, total=False): - id: Required[str] - """The unique identifier for the input field from the application. - - All input types from the No-code application are supported (i.e. Text input, - Dropdown, File upload, Image input). The identifier should be the name of the - input type. - """ - value: Required[List[str]] - """The value for the input field. +class Input(TypedDict, total=False): + content: str + """The input content to be processed.""" - If file is required you will need to pass a `file_id`. See - [here](https://dev.writer.com/api-guides/api-reference/file-api/upload-files) - for the Files API. - """ + input_id: str + """A unique identifier for the input object.""" diff --git a/src/writerai/types/applications/job_create_response.py b/src/writerai/types/applications/job_create_response.py index f83502c2..567785a6 100644 --- a/src/writerai/types/applications/job_create_response.py +++ b/src/writerai/types/applications/job_create_response.py @@ -1,7 +1,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from typing import Optional from datetime import datetime -from typing_extensions import Literal from ..._models import BaseModel @@ -9,11 +9,14 @@ class JobCreateResponse(BaseModel): - id: str + job_id: str """The unique identifier for the async job created.""" - created_at: datetime + application_id: Optional[str] = None + """The ID of the application associated with this job.""" + + created_at: Optional[datetime] = None """The timestamp when the job was created.""" - status: Literal["in_progress", "failed", "completed"] - """The status of the job.""" + status: Optional[str] = None + """The initial status of the job (e.g., 'queued').""" diff --git a/src/writerai/types/applications/job_list_params.py b/src/writerai/types/applications/job_list_params.py index dfb85b65..ffdea204 100644 --- a/src/writerai/types/applications/job_list_params.py +++ b/src/writerai/types/applications/job_list_params.py @@ -2,17 +2,40 @@ from __future__ import annotations -from typing_extensions import Literal, TypedDict +from typing import Union, Iterable +from datetime import datetime +from typing_extensions import Required, Annotated, TypedDict -__all__ = ["JobListParams"] +from ..._utils import PropertyInfo + +__all__ = ["JobListParams", "Status", "StatusJob"] class JobListParams(TypedDict, total=False): limit: int - """The pagination limit for retrieving the jobs.""" offset: int - """The pagination offset for retrieving the jobs.""" - status: Literal["in_progress", "failed", "completed"] - """The status of the job.""" + status: Status + + +class StatusJob(TypedDict, total=False): + created_at: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")] + """The timestamp when the job was created.""" + + job_id: str + """The unique identifier for the job.""" + + result: str + """The result of the completed job, if applicable.""" + + status: str + """The current status of the job.""" + + updated_at: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")] + """The timestamp when the job was last updated.""" + + +class Status(TypedDict, total=False): + jobs: Required[Iterable[StatusJob]] + """A list of jobs associated with the application.""" diff --git a/src/writerai/types/applications/job_list_response.py b/src/writerai/types/applications/job_list_response.py index dad66049..eb591ec9 100644 --- a/src/writerai/types/applications/job_list_response.py +++ b/src/writerai/types/applications/job_list_response.py @@ -2,39 +2,29 @@ from typing import List, Optional from datetime import datetime -from typing_extensions import Literal from ..._models import BaseModel -from ..application_generate_content_response import ApplicationGenerateContentResponse -__all__ = ["JobListResponse", "Result"] +__all__ = ["JobListResponse", "Job"] -class Result(BaseModel): - id: str - """The unique identifier for the job.""" - - application_id: str - """The ID of the application associated with this job.""" - - created_at: datetime +class Job(BaseModel): + created_at: Optional[datetime] = None """The timestamp when the job was created.""" - status: Literal["in_progress", "failed", "completed"] - """The status of the job.""" - - completed_at: Optional[datetime] = None - """The timestamp when the job was completed.""" + job_id: Optional[str] = None + """The unique identifier for the job.""" - data: Optional[ApplicationGenerateContentResponse] = None + result: Optional[str] = None """The result of the completed job, if applicable.""" - error: Optional[str] = None - """The error message if the job failed.""" + status: Optional[str] = None + """The current status of the job.""" updated_at: Optional[datetime] = None """The timestamp when the job was last updated.""" class JobListResponse(BaseModel): - result: Optional[List[Result]] = None + jobs: List[Job] + """A list of jobs associated with the application.""" diff --git a/src/writerai/types/applications/job_retrieve_response.py b/src/writerai/types/applications/job_retrieve_response.py index 8de6ad3e..bd8e0d1f 100644 --- a/src/writerai/types/applications/job_retrieve_response.py +++ b/src/writerai/types/applications/job_retrieve_response.py @@ -1,36 +1,30 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional +from typing import List, Optional from datetime import datetime -from typing_extensions import Literal from ..._models import BaseModel -from ..application_generate_content_response import ApplicationGenerateContentResponse -__all__ = ["JobRetrieveResponse"] +__all__ = ["JobRetrieveResponse", "Job"] -class JobRetrieveResponse(BaseModel): - id: str - """The unique identifier for the job.""" - - application_id: str - """The ID of the application associated with this job.""" - - created_at: datetime +class Job(BaseModel): + created_at: Optional[datetime] = None """The timestamp when the job was created.""" - status: Literal["in_progress", "failed", "completed"] - """The status of the job.""" - - completed_at: Optional[datetime] = None - """The timestamp when the job was completed.""" + job_id: Optional[str] = None + """The unique identifier for the job.""" - data: Optional[ApplicationGenerateContentResponse] = None + result: Optional[str] = None """The result of the completed job, if applicable.""" - error: Optional[str] = None - """The error message if the job failed.""" + status: Optional[str] = None + """The current status of the job.""" updated_at: Optional[datetime] = None """The timestamp when the job was last updated.""" + + +class JobRetrieveResponse(BaseModel): + jobs: List[Job] + """A list of jobs associated with the application.""" diff --git a/src/writerai/types/applications/job_retry_response.py b/src/writerai/types/applications/job_retry_response.py index 9555ce6d..d9b4765a 100644 --- a/src/writerai/types/applications/job_retry_response.py +++ b/src/writerai/types/applications/job_retry_response.py @@ -1,7 +1,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from typing import Optional from datetime import datetime -from typing_extensions import Literal from ..._models import BaseModel @@ -9,11 +9,14 @@ class JobRetryResponse(BaseModel): - id: str + job_id: str """The unique identifier for the async job created.""" - created_at: datetime + application_id: Optional[str] = None + """The ID of the application associated with this job.""" + + created_at: Optional[datetime] = None """The timestamp when the job was created.""" - status: Literal["in_progress", "failed", "completed"] - """The status of the job.""" + status: Optional[str] = None + """The initial status of the job (e.g., 'queued').""" diff --git a/tests/api_resources/applications/test_jobs.py b/tests/api_resources/applications/test_jobs.py index 29dd579d..a10ad56b 100644 --- a/tests/api_resources/applications/test_jobs.py +++ b/tests/api_resources/applications/test_jobs.py @@ -9,6 +9,7 @@ from writerai import Writer, AsyncWriter from tests.utils import assert_matches_type +from writerai._utils import parse_datetime from writerai.types.applications import ( JobListResponse, JobRetryResponse, @@ -24,14 +25,23 @@ class TestJobs: @parametrize def test_method_create(self, client: Writer) -> None: + job = client.applications.jobs.create( + application_id="application_id", + inputs=[{}], + ) + assert_matches_type(JobCreateResponse, job, path=["response"]) + + @parametrize + def test_method_create_with_all_params(self, client: Writer) -> None: job = client.applications.jobs.create( application_id="application_id", inputs=[ { - "id": "id", - "value": ["string"], + "content": "content", + "input_id": "input_id", } ], + metadata={"foo": "string"}, ) assert_matches_type(JobCreateResponse, job, path=["response"]) @@ -39,12 +49,7 @@ def test_method_create(self, client: Writer) -> None: def test_raw_response_create(self, client: Writer) -> None: response = client.applications.jobs.with_raw_response.create( application_id="application_id", - inputs=[ - { - "id": "id", - "value": ["string"], - } - ], + inputs=[{}], ) assert response.is_closed is True @@ -56,12 +61,7 @@ def test_raw_response_create(self, client: Writer) -> None: def test_streaming_response_create(self, client: Writer) -> None: with client.applications.jobs.with_streaming_response.create( application_id="application_id", - inputs=[ - { - "id": "id", - "value": ["string"], - } - ], + inputs=[{}], ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -76,12 +76,7 @@ def test_path_params_create(self, client: Writer) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `application_id` but received ''"): client.applications.jobs.with_raw_response.create( application_id="", - inputs=[ - { - "id": "id", - "value": ["string"], - } - ], + inputs=[{}], ) @parametrize @@ -135,7 +130,17 @@ def test_method_list_with_all_params(self, client: Writer) -> None: application_id="application_id", limit=0, offset=0, - status="in_progress", + status={ + "jobs": [ + { + "created_at": parse_datetime("2019-12-27T18:11:19.117Z"), + "job_id": "job_id", + "result": "result", + "status": "status", + "updated_at": parse_datetime("2019-12-27T18:11:19.117Z"), + } + ] + }, ) assert_matches_type(JobListResponse, job, path=["response"]) @@ -214,14 +219,23 @@ class TestAsyncJobs: @parametrize async def test_method_create(self, async_client: AsyncWriter) -> None: + job = await async_client.applications.jobs.create( + application_id="application_id", + inputs=[{}], + ) + assert_matches_type(JobCreateResponse, job, path=["response"]) + + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncWriter) -> None: job = await async_client.applications.jobs.create( application_id="application_id", inputs=[ { - "id": "id", - "value": ["string"], + "content": "content", + "input_id": "input_id", } ], + metadata={"foo": "string"}, ) assert_matches_type(JobCreateResponse, job, path=["response"]) @@ -229,12 +243,7 @@ async def test_method_create(self, async_client: AsyncWriter) -> None: async def test_raw_response_create(self, async_client: AsyncWriter) -> None: response = await async_client.applications.jobs.with_raw_response.create( application_id="application_id", - inputs=[ - { - "id": "id", - "value": ["string"], - } - ], + inputs=[{}], ) assert response.is_closed is True @@ -246,12 +255,7 @@ async def test_raw_response_create(self, async_client: AsyncWriter) -> None: async def test_streaming_response_create(self, async_client: AsyncWriter) -> None: async with async_client.applications.jobs.with_streaming_response.create( application_id="application_id", - inputs=[ - { - "id": "id", - "value": ["string"], - } - ], + inputs=[{}], ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -266,12 +270,7 @@ async def test_path_params_create(self, async_client: AsyncWriter) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `application_id` but received ''"): await async_client.applications.jobs.with_raw_response.create( application_id="", - inputs=[ - { - "id": "id", - "value": ["string"], - } - ], + inputs=[{}], ) @parametrize @@ -325,7 +324,17 @@ async def test_method_list_with_all_params(self, async_client: AsyncWriter) -> N application_id="application_id", limit=0, offset=0, - status="in_progress", + status={ + "jobs": [ + { + "created_at": parse_datetime("2019-12-27T18:11:19.117Z"), + "job_id": "job_id", + "result": "result", + "status": "status", + "updated_at": parse_datetime("2019-12-27T18:11:19.117Z"), + } + ] + }, ) assert_matches_type(JobListResponse, job, path=["response"]) From 65405ec87de4e2c45b185a0c8bfdd3e21f312b88 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 29 Jan 2025 22:41:58 +0000 Subject: [PATCH 165/399] docs(api): updates to API spec (#170) --- .stats.yml | 2 +- api.md | 2 +- src/writerai/resources/applications/graphs.py | 14 ++- src/writerai/resources/applications/jobs.py | 62 +++++----- .../types/applications/graph_update_params.py | 6 +- .../types/applications/job_create_params.py | 23 ++-- .../types/applications/job_create_response.py | 13 +-- .../types/applications/job_list_params.py | 35 +----- .../types/applications/job_list_response.py | 34 +++--- .../applications/job_retrieve_response.py | 34 +++--- .../types/applications/job_retry_response.py | 13 +-- tests/api_resources/applications/test_jobs.py | 110 ++++++++---------- 12 files changed, 169 insertions(+), 179 deletions(-) diff --git a/.stats.yml b/.stats.yml index e1f33816..6f4a8723 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 27 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-95e5c41bc4917566fc6ee1f849795bac2e34e5609afd25bb927252ac7e33e2f0.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-ffd1e266059dd5eabc5a0a437bbf4c7b06d497152e076a91e0cc25243a485344.yml diff --git a/api.md b/api.md index 76d757ea..5fed86c7 100644 --- a/api.md +++ b/api.md @@ -47,7 +47,7 @@ Methods: - client.applications.jobs.create(application_id, \*\*params) -> JobCreateResponse - client.applications.jobs.retrieve(job_id) -> JobRetrieveResponse -- client.applications.jobs.list(application_id, \*\*params) -> JobListResponse +- client.applications.jobs.list(application_id, \*\*params) -> SyncApplicationJobsOffset[JobListResponse] - client.applications.jobs.retry(job_id) -> JobRetryResponse ## Graphs diff --git a/src/writerai/resources/applications/graphs.py b/src/writerai/resources/applications/graphs.py index 3a739d6e..f393f660 100644 --- a/src/writerai/resources/applications/graphs.py +++ b/src/writerai/resources/applications/graphs.py @@ -59,10 +59,13 @@ def update( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ApplicationGraphsResponse: """ - Associate graphs with a no-code chat application via API. + Updates the graphs listed and associates them with the no-code chat app to be + used. Args: - graph_ids: A list of graph IDs to associate with the application. + graph_ids: A list of graph IDs to associate with the application. Note that this will + replace the existing list of graphs associated with the application, not add to + it. extra_headers: Send extra headers @@ -150,10 +153,13 @@ async def update( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ApplicationGraphsResponse: """ - Associate graphs with a no-code chat application via API. + Updates the graphs listed and associates them with the no-code chat app to be + used. Args: - graph_ids: A list of graph IDs to associate with the application. + graph_ids: A list of graph IDs to associate with the application. Note that this will + replace the existing list of graphs associated with the application, not add to + it. extra_headers: Send extra headers diff --git a/src/writerai/resources/applications/jobs.py b/src/writerai/resources/applications/jobs.py index 19f2f3ee..88dc8aa5 100644 --- a/src/writerai/resources/applications/jobs.py +++ b/src/writerai/resources/applications/jobs.py @@ -2,7 +2,8 @@ from __future__ import annotations -from typing import Dict, Iterable +from typing import Iterable +from typing_extensions import Literal import httpx @@ -19,7 +20,8 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from ..._base_client import make_request_options +from ...pagination import SyncApplicationJobsOffset, AsyncApplicationJobsOffset +from ..._base_client import AsyncPaginator, make_request_options from ...types.applications import job_list_params, job_create_params from ...types.applications.job_list_response import JobListResponse from ...types.applications.job_retry_response import JobRetryResponse @@ -54,7 +56,6 @@ def create( application_id: str, *, inputs: Iterable[job_create_params.Input], - metadata: Dict[str, str] | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -68,8 +69,6 @@ def create( Args: inputs: A list of input objects to generate content for. - metadata: Optional metadata for the generation request. - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -82,13 +81,7 @@ def create( raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") return self._post( f"/v1/applications/{application_id}/jobs", - body=maybe_transform( - { - "inputs": inputs, - "metadata": metadata, - }, - job_create_params.JobCreateParams, - ), + body=maybe_transform({"inputs": inputs}, job_create_params.JobCreateParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -134,19 +127,25 @@ def list( *, limit: int | NotGiven = NOT_GIVEN, offset: int | NotGiven = NOT_GIVEN, - status: job_list_params.Status | NotGiven = NOT_GIVEN, + status: Literal["in_progress", "failed", "completed"] | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> JobListResponse: + ) -> SyncApplicationJobsOffset[JobListResponse]: """ Retrieve all jobs created via the async API, linked to the provided application ID (or alias). Args: + limit: The pagination limit for retrieving the jobs. + + offset: The pagination offset for retrieving the jobs. + + status: The status of the job. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -157,8 +156,9 @@ def list( """ if not application_id: raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") - return self._get( + return self._get_api_list( f"/v1/applications/{application_id}/jobs", + page=SyncApplicationJobsOffset[JobListResponse], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -173,7 +173,7 @@ def list( job_list_params.JobListParams, ), ), - cast_to=JobListResponse, + model=JobListResponse, ) def retry( @@ -236,7 +236,6 @@ async def create( application_id: str, *, inputs: Iterable[job_create_params.Input], - metadata: Dict[str, str] | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -250,8 +249,6 @@ async def create( Args: inputs: A list of input objects to generate content for. - metadata: Optional metadata for the generation request. - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -264,13 +261,7 @@ async def create( raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") return await self._post( f"/v1/applications/{application_id}/jobs", - body=await async_maybe_transform( - { - "inputs": inputs, - "metadata": metadata, - }, - job_create_params.JobCreateParams, - ), + body=await async_maybe_transform({"inputs": inputs}, job_create_params.JobCreateParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -310,25 +301,31 @@ async def retrieve( cast_to=JobRetrieveResponse, ) - async def list( + def list( self, application_id: str, *, limit: int | NotGiven = NOT_GIVEN, offset: int | NotGiven = NOT_GIVEN, - status: job_list_params.Status | NotGiven = NOT_GIVEN, + status: Literal["in_progress", "failed", "completed"] | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> JobListResponse: + ) -> AsyncPaginator[JobListResponse, AsyncApplicationJobsOffset[JobListResponse]]: """ Retrieve all jobs created via the async API, linked to the provided application ID (or alias). Args: + limit: The pagination limit for retrieving the jobs. + + offset: The pagination offset for retrieving the jobs. + + status: The status of the job. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -339,14 +336,15 @@ async def list( """ if not application_id: raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") - return await self._get( + return self._get_api_list( f"/v1/applications/{application_id}/jobs", + page=AsyncApplicationJobsOffset[JobListResponse], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout, - query=await async_maybe_transform( + query=maybe_transform( { "limit": limit, "offset": offset, @@ -355,7 +353,7 @@ async def list( job_list_params.JobListParams, ), ), - cast_to=JobListResponse, + model=JobListResponse, ) async def retry( diff --git a/src/writerai/types/applications/graph_update_params.py b/src/writerai/types/applications/graph_update_params.py index 1e37bf4e..ea7be169 100644 --- a/src/writerai/types/applications/graph_update_params.py +++ b/src/writerai/types/applications/graph_update_params.py @@ -10,4 +10,8 @@ class GraphUpdateParams(TypedDict, total=False): graph_ids: Required[List[str]] - """A list of graph IDs to associate with the application.""" + """A list of graph IDs to associate with the application. + + Note that this will replace the existing list of graphs associated with the + application, not add to it. + """ diff --git a/src/writerai/types/applications/job_create_params.py b/src/writerai/types/applications/job_create_params.py index 2cc98aeb..86c6b048 100644 --- a/src/writerai/types/applications/job_create_params.py +++ b/src/writerai/types/applications/job_create_params.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Dict, Iterable +from typing import List, Iterable from typing_extensions import Required, TypedDict __all__ = ["JobCreateParams", "Input"] @@ -12,13 +12,20 @@ class JobCreateParams(TypedDict, total=False): inputs: Required[Iterable[Input]] """A list of input objects to generate content for.""" - metadata: Dict[str, str] - """Optional metadata for the generation request.""" - class Input(TypedDict, total=False): - content: str - """The input content to be processed.""" + id: Required[str] + """The unique identifier for the input field from the application. + + All input types from the No-code application are supported (i.e. Text input, + Dropdown, File upload, Image input). The identifier should be the name of the + input type. + """ + + value: Required[List[str]] + """The value for the input field. - input_id: str - """A unique identifier for the input object.""" + If file is required you will need to pass a `file_id`. See + [here](https://dev.writer.com/api-guides/api-reference/file-api/upload-files) + for the Files API. + """ diff --git a/src/writerai/types/applications/job_create_response.py b/src/writerai/types/applications/job_create_response.py index 567785a6..f83502c2 100644 --- a/src/writerai/types/applications/job_create_response.py +++ b/src/writerai/types/applications/job_create_response.py @@ -1,7 +1,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional from datetime import datetime +from typing_extensions import Literal from ..._models import BaseModel @@ -9,14 +9,11 @@ class JobCreateResponse(BaseModel): - job_id: str + id: str """The unique identifier for the async job created.""" - application_id: Optional[str] = None - """The ID of the application associated with this job.""" - - created_at: Optional[datetime] = None + created_at: datetime """The timestamp when the job was created.""" - status: Optional[str] = None - """The initial status of the job (e.g., 'queued').""" + status: Literal["in_progress", "failed", "completed"] + """The status of the job.""" diff --git a/src/writerai/types/applications/job_list_params.py b/src/writerai/types/applications/job_list_params.py index ffdea204..dfb85b65 100644 --- a/src/writerai/types/applications/job_list_params.py +++ b/src/writerai/types/applications/job_list_params.py @@ -2,40 +2,17 @@ from __future__ import annotations -from typing import Union, Iterable -from datetime import datetime -from typing_extensions import Required, Annotated, TypedDict +from typing_extensions import Literal, TypedDict -from ..._utils import PropertyInfo - -__all__ = ["JobListParams", "Status", "StatusJob"] +__all__ = ["JobListParams"] class JobListParams(TypedDict, total=False): limit: int + """The pagination limit for retrieving the jobs.""" offset: int + """The pagination offset for retrieving the jobs.""" - status: Status - - -class StatusJob(TypedDict, total=False): - created_at: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")] - """The timestamp when the job was created.""" - - job_id: str - """The unique identifier for the job.""" - - result: str - """The result of the completed job, if applicable.""" - - status: str - """The current status of the job.""" - - updated_at: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")] - """The timestamp when the job was last updated.""" - - -class Status(TypedDict, total=False): - jobs: Required[Iterable[StatusJob]] - """A list of jobs associated with the application.""" + status: Literal["in_progress", "failed", "completed"] + """The status of the job.""" diff --git a/src/writerai/types/applications/job_list_response.py b/src/writerai/types/applications/job_list_response.py index eb591ec9..83b6199d 100644 --- a/src/writerai/types/applications/job_list_response.py +++ b/src/writerai/types/applications/job_list_response.py @@ -1,30 +1,36 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List, Optional +from typing import Optional from datetime import datetime +from typing_extensions import Literal from ..._models import BaseModel +from ..application_generate_content_response import ApplicationGenerateContentResponse -__all__ = ["JobListResponse", "Job"] +__all__ = ["JobListResponse"] -class Job(BaseModel): - created_at: Optional[datetime] = None +class JobListResponse(BaseModel): + id: str + """The unique identifier for the job.""" + + application_id: str + """The ID of the application associated with this job.""" + + created_at: datetime """The timestamp when the job was created.""" - job_id: Optional[str] = None - """The unique identifier for the job.""" + status: Literal["in_progress", "failed", "completed"] + """The status of the job.""" - result: Optional[str] = None + completed_at: Optional[datetime] = None + """The timestamp when the job was completed.""" + + data: Optional[ApplicationGenerateContentResponse] = None """The result of the completed job, if applicable.""" - status: Optional[str] = None - """The current status of the job.""" + error: Optional[str] = None + """The error message if the job failed.""" updated_at: Optional[datetime] = None """The timestamp when the job was last updated.""" - - -class JobListResponse(BaseModel): - jobs: List[Job] - """A list of jobs associated with the application.""" diff --git a/src/writerai/types/applications/job_retrieve_response.py b/src/writerai/types/applications/job_retrieve_response.py index bd8e0d1f..8de6ad3e 100644 --- a/src/writerai/types/applications/job_retrieve_response.py +++ b/src/writerai/types/applications/job_retrieve_response.py @@ -1,30 +1,36 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List, Optional +from typing import Optional from datetime import datetime +from typing_extensions import Literal from ..._models import BaseModel +from ..application_generate_content_response import ApplicationGenerateContentResponse -__all__ = ["JobRetrieveResponse", "Job"] +__all__ = ["JobRetrieveResponse"] -class Job(BaseModel): - created_at: Optional[datetime] = None +class JobRetrieveResponse(BaseModel): + id: str + """The unique identifier for the job.""" + + application_id: str + """The ID of the application associated with this job.""" + + created_at: datetime """The timestamp when the job was created.""" - job_id: Optional[str] = None - """The unique identifier for the job.""" + status: Literal["in_progress", "failed", "completed"] + """The status of the job.""" - result: Optional[str] = None + completed_at: Optional[datetime] = None + """The timestamp when the job was completed.""" + + data: Optional[ApplicationGenerateContentResponse] = None """The result of the completed job, if applicable.""" - status: Optional[str] = None - """The current status of the job.""" + error: Optional[str] = None + """The error message if the job failed.""" updated_at: Optional[datetime] = None """The timestamp when the job was last updated.""" - - -class JobRetrieveResponse(BaseModel): - jobs: List[Job] - """A list of jobs associated with the application.""" diff --git a/src/writerai/types/applications/job_retry_response.py b/src/writerai/types/applications/job_retry_response.py index d9b4765a..9555ce6d 100644 --- a/src/writerai/types/applications/job_retry_response.py +++ b/src/writerai/types/applications/job_retry_response.py @@ -1,7 +1,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional from datetime import datetime +from typing_extensions import Literal from ..._models import BaseModel @@ -9,14 +9,11 @@ class JobRetryResponse(BaseModel): - job_id: str + id: str """The unique identifier for the async job created.""" - application_id: Optional[str] = None - """The ID of the application associated with this job.""" - - created_at: Optional[datetime] = None + created_at: datetime """The timestamp when the job was created.""" - status: Optional[str] = None - """The initial status of the job (e.g., 'queued').""" + status: Literal["in_progress", "failed", "completed"] + """The status of the job.""" diff --git a/tests/api_resources/applications/test_jobs.py b/tests/api_resources/applications/test_jobs.py index a10ad56b..96a5db3c 100644 --- a/tests/api_resources/applications/test_jobs.py +++ b/tests/api_resources/applications/test_jobs.py @@ -9,7 +9,7 @@ from writerai import Writer, AsyncWriter from tests.utils import assert_matches_type -from writerai._utils import parse_datetime +from writerai.pagination import SyncApplicationJobsOffset, AsyncApplicationJobsOffset from writerai.types.applications import ( JobListResponse, JobRetryResponse, @@ -25,23 +25,14 @@ class TestJobs: @parametrize def test_method_create(self, client: Writer) -> None: - job = client.applications.jobs.create( - application_id="application_id", - inputs=[{}], - ) - assert_matches_type(JobCreateResponse, job, path=["response"]) - - @parametrize - def test_method_create_with_all_params(self, client: Writer) -> None: job = client.applications.jobs.create( application_id="application_id", inputs=[ { - "content": "content", - "input_id": "input_id", + "id": "id", + "value": ["string"], } ], - metadata={"foo": "string"}, ) assert_matches_type(JobCreateResponse, job, path=["response"]) @@ -49,7 +40,12 @@ def test_method_create_with_all_params(self, client: Writer) -> None: def test_raw_response_create(self, client: Writer) -> None: response = client.applications.jobs.with_raw_response.create( application_id="application_id", - inputs=[{}], + inputs=[ + { + "id": "id", + "value": ["string"], + } + ], ) assert response.is_closed is True @@ -61,7 +57,12 @@ def test_raw_response_create(self, client: Writer) -> None: def test_streaming_response_create(self, client: Writer) -> None: with client.applications.jobs.with_streaming_response.create( application_id="application_id", - inputs=[{}], + inputs=[ + { + "id": "id", + "value": ["string"], + } + ], ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -76,7 +77,12 @@ def test_path_params_create(self, client: Writer) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `application_id` but received ''"): client.applications.jobs.with_raw_response.create( application_id="", - inputs=[{}], + inputs=[ + { + "id": "id", + "value": ["string"], + } + ], ) @parametrize @@ -122,7 +128,7 @@ def test_method_list(self, client: Writer) -> None: job = client.applications.jobs.list( application_id="application_id", ) - assert_matches_type(JobListResponse, job, path=["response"]) + assert_matches_type(SyncApplicationJobsOffset[JobListResponse], job, path=["response"]) @parametrize def test_method_list_with_all_params(self, client: Writer) -> None: @@ -130,19 +136,9 @@ def test_method_list_with_all_params(self, client: Writer) -> None: application_id="application_id", limit=0, offset=0, - status={ - "jobs": [ - { - "created_at": parse_datetime("2019-12-27T18:11:19.117Z"), - "job_id": "job_id", - "result": "result", - "status": "status", - "updated_at": parse_datetime("2019-12-27T18:11:19.117Z"), - } - ] - }, + status="in_progress", ) - assert_matches_type(JobListResponse, job, path=["response"]) + assert_matches_type(SyncApplicationJobsOffset[JobListResponse], job, path=["response"]) @parametrize def test_raw_response_list(self, client: Writer) -> None: @@ -153,7 +149,7 @@ def test_raw_response_list(self, client: Writer) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" job = response.parse() - assert_matches_type(JobListResponse, job, path=["response"]) + assert_matches_type(SyncApplicationJobsOffset[JobListResponse], job, path=["response"]) @parametrize def test_streaming_response_list(self, client: Writer) -> None: @@ -164,7 +160,7 @@ def test_streaming_response_list(self, client: Writer) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" job = response.parse() - assert_matches_type(JobListResponse, job, path=["response"]) + assert_matches_type(SyncApplicationJobsOffset[JobListResponse], job, path=["response"]) assert cast(Any, response.is_closed) is True @@ -219,23 +215,14 @@ class TestAsyncJobs: @parametrize async def test_method_create(self, async_client: AsyncWriter) -> None: - job = await async_client.applications.jobs.create( - application_id="application_id", - inputs=[{}], - ) - assert_matches_type(JobCreateResponse, job, path=["response"]) - - @parametrize - async def test_method_create_with_all_params(self, async_client: AsyncWriter) -> None: job = await async_client.applications.jobs.create( application_id="application_id", inputs=[ { - "content": "content", - "input_id": "input_id", + "id": "id", + "value": ["string"], } ], - metadata={"foo": "string"}, ) assert_matches_type(JobCreateResponse, job, path=["response"]) @@ -243,7 +230,12 @@ async def test_method_create_with_all_params(self, async_client: AsyncWriter) -> async def test_raw_response_create(self, async_client: AsyncWriter) -> None: response = await async_client.applications.jobs.with_raw_response.create( application_id="application_id", - inputs=[{}], + inputs=[ + { + "id": "id", + "value": ["string"], + } + ], ) assert response.is_closed is True @@ -255,7 +247,12 @@ async def test_raw_response_create(self, async_client: AsyncWriter) -> None: async def test_streaming_response_create(self, async_client: AsyncWriter) -> None: async with async_client.applications.jobs.with_streaming_response.create( application_id="application_id", - inputs=[{}], + inputs=[ + { + "id": "id", + "value": ["string"], + } + ], ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -270,7 +267,12 @@ async def test_path_params_create(self, async_client: AsyncWriter) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `application_id` but received ''"): await async_client.applications.jobs.with_raw_response.create( application_id="", - inputs=[{}], + inputs=[ + { + "id": "id", + "value": ["string"], + } + ], ) @parametrize @@ -316,7 +318,7 @@ async def test_method_list(self, async_client: AsyncWriter) -> None: job = await async_client.applications.jobs.list( application_id="application_id", ) - assert_matches_type(JobListResponse, job, path=["response"]) + assert_matches_type(AsyncApplicationJobsOffset[JobListResponse], job, path=["response"]) @parametrize async def test_method_list_with_all_params(self, async_client: AsyncWriter) -> None: @@ -324,19 +326,9 @@ async def test_method_list_with_all_params(self, async_client: AsyncWriter) -> N application_id="application_id", limit=0, offset=0, - status={ - "jobs": [ - { - "created_at": parse_datetime("2019-12-27T18:11:19.117Z"), - "job_id": "job_id", - "result": "result", - "status": "status", - "updated_at": parse_datetime("2019-12-27T18:11:19.117Z"), - } - ] - }, + status="in_progress", ) - assert_matches_type(JobListResponse, job, path=["response"]) + assert_matches_type(AsyncApplicationJobsOffset[JobListResponse], job, path=["response"]) @parametrize async def test_raw_response_list(self, async_client: AsyncWriter) -> None: @@ -347,7 +339,7 @@ async def test_raw_response_list(self, async_client: AsyncWriter) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" job = await response.parse() - assert_matches_type(JobListResponse, job, path=["response"]) + assert_matches_type(AsyncApplicationJobsOffset[JobListResponse], job, path=["response"]) @parametrize async def test_streaming_response_list(self, async_client: AsyncWriter) -> None: @@ -358,7 +350,7 @@ async def test_streaming_response_list(self, async_client: AsyncWriter) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" job = await response.parse() - assert_matches_type(JobListResponse, job, path=["response"]) + assert_matches_type(AsyncApplicationJobsOffset[JobListResponse], job, path=["response"]) assert cast(Any, response.is_closed) is True From d60bb7d2de64be0760cdb0cdbe006955db860b58 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 29 Jan 2025 23:03:55 +0000 Subject: [PATCH 166/399] feat(api): add types for application jobs (#171) --- api.md | 8 ++--- src/writerai/resources/applications/jobs.py | 23 ++++++------ src/writerai/types/applications/__init__.py | 4 +-- ...=> application_generate_async_response.py} | 4 +-- .../application_jobs_list_response.py | 27 ++++++++++++++ .../applications/job_retrieve_response.py | 36 ------------------- tests/api_resources/applications/test_jobs.py | 31 ++++++++-------- 7 files changed, 61 insertions(+), 72 deletions(-) rename src/writerai/types/applications/{job_list_response.py => application_generate_async_response.py} (91%) create mode 100644 src/writerai/types/applications/application_jobs_list_response.py delete mode 100644 src/writerai/types/applications/job_retrieve_response.py diff --git a/api.md b/api.md index 5fed86c7..44d29873 100644 --- a/api.md +++ b/api.md @@ -36,9 +36,9 @@ Types: ```python from writerai.types.applications import ( + ApplicationGenerateAsyncResponse, + ApplicationJobsListResponse, JobCreateResponse, - JobRetrieveResponse, - JobListResponse, JobRetryResponse, ) ``` @@ -46,8 +46,8 @@ from writerai.types.applications import ( Methods: - client.applications.jobs.create(application_id, \*\*params) -> JobCreateResponse -- client.applications.jobs.retrieve(job_id) -> JobRetrieveResponse -- client.applications.jobs.list(application_id, \*\*params) -> SyncApplicationJobsOffset[JobListResponse] +- client.applications.jobs.retrieve(job_id) -> ApplicationGenerateAsyncResponse +- client.applications.jobs.list(application_id, \*\*params) -> SyncApplicationJobsOffset[ApplicationGenerateAsyncResponse] - client.applications.jobs.retry(job_id) -> JobRetryResponse ## Graphs diff --git a/src/writerai/resources/applications/jobs.py b/src/writerai/resources/applications/jobs.py index 88dc8aa5..779402c6 100644 --- a/src/writerai/resources/applications/jobs.py +++ b/src/writerai/resources/applications/jobs.py @@ -23,10 +23,9 @@ from ...pagination import SyncApplicationJobsOffset, AsyncApplicationJobsOffset from ..._base_client import AsyncPaginator, make_request_options from ...types.applications import job_list_params, job_create_params -from ...types.applications.job_list_response import JobListResponse from ...types.applications.job_retry_response import JobRetryResponse from ...types.applications.job_create_response import JobCreateResponse -from ...types.applications.job_retrieve_response import JobRetrieveResponse +from ...types.applications.application_generate_async_response import ApplicationGenerateAsyncResponse __all__ = ["JobsResource", "AsyncJobsResource"] @@ -98,7 +97,7 @@ def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> JobRetrieveResponse: + ) -> ApplicationGenerateAsyncResponse: """ Retrieves a single job created via the Async API. @@ -118,7 +117,7 @@ def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=JobRetrieveResponse, + cast_to=ApplicationGenerateAsyncResponse, ) def list( @@ -134,7 +133,7 @@ def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SyncApplicationJobsOffset[JobListResponse]: + ) -> SyncApplicationJobsOffset[ApplicationGenerateAsyncResponse]: """ Retrieve all jobs created via the async API, linked to the provided application ID (or alias). @@ -158,7 +157,7 @@ def list( raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") return self._get_api_list( f"/v1/applications/{application_id}/jobs", - page=SyncApplicationJobsOffset[JobListResponse], + page=SyncApplicationJobsOffset[ApplicationGenerateAsyncResponse], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -173,7 +172,7 @@ def list( job_list_params.JobListParams, ), ), - model=JobListResponse, + model=ApplicationGenerateAsyncResponse, ) def retry( @@ -278,7 +277,7 @@ async def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> JobRetrieveResponse: + ) -> ApplicationGenerateAsyncResponse: """ Retrieves a single job created via the Async API. @@ -298,7 +297,7 @@ async def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=JobRetrieveResponse, + cast_to=ApplicationGenerateAsyncResponse, ) def list( @@ -314,7 +313,7 @@ def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AsyncPaginator[JobListResponse, AsyncApplicationJobsOffset[JobListResponse]]: + ) -> AsyncPaginator[ApplicationGenerateAsyncResponse, AsyncApplicationJobsOffset[ApplicationGenerateAsyncResponse]]: """ Retrieve all jobs created via the async API, linked to the provided application ID (or alias). @@ -338,7 +337,7 @@ def list( raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") return self._get_api_list( f"/v1/applications/{application_id}/jobs", - page=AsyncApplicationJobsOffset[JobListResponse], + page=AsyncApplicationJobsOffset[ApplicationGenerateAsyncResponse], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -353,7 +352,7 @@ def list( job_list_params.JobListParams, ), ), - model=JobListResponse, + model=ApplicationGenerateAsyncResponse, ) async def retry( diff --git a/src/writerai/types/applications/__init__.py b/src/writerai/types/applications/__init__.py index c0981e1c..92c8a570 100644 --- a/src/writerai/types/applications/__init__.py +++ b/src/writerai/types/applications/__init__.py @@ -4,9 +4,9 @@ from .job_list_params import JobListParams as JobListParams from .job_create_params import JobCreateParams as JobCreateParams -from .job_list_response import JobListResponse as JobListResponse from .job_retry_response import JobRetryResponse as JobRetryResponse from .graph_update_params import GraphUpdateParams as GraphUpdateParams from .job_create_response import JobCreateResponse as JobCreateResponse -from .job_retrieve_response import JobRetrieveResponse as JobRetrieveResponse from .application_graphs_response import ApplicationGraphsResponse as ApplicationGraphsResponse +from .application_jobs_list_response import ApplicationJobsListResponse as ApplicationJobsListResponse +from .application_generate_async_response import ApplicationGenerateAsyncResponse as ApplicationGenerateAsyncResponse diff --git a/src/writerai/types/applications/job_list_response.py b/src/writerai/types/applications/application_generate_async_response.py similarity index 91% rename from src/writerai/types/applications/job_list_response.py rename to src/writerai/types/applications/application_generate_async_response.py index 83b6199d..08afe291 100644 --- a/src/writerai/types/applications/job_list_response.py +++ b/src/writerai/types/applications/application_generate_async_response.py @@ -7,10 +7,10 @@ from ..._models import BaseModel from ..application_generate_content_response import ApplicationGenerateContentResponse -__all__ = ["JobListResponse"] +__all__ = ["ApplicationGenerateAsyncResponse"] -class JobListResponse(BaseModel): +class ApplicationGenerateAsyncResponse(BaseModel): id: str """The unique identifier for the job.""" diff --git a/src/writerai/types/applications/application_jobs_list_response.py b/src/writerai/types/applications/application_jobs_list_response.py new file mode 100644 index 00000000..f6e92126 --- /dev/null +++ b/src/writerai/types/applications/application_jobs_list_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 pydantic import Field as FieldInfo + +from ..._models import BaseModel +from .application_generate_async_response import ApplicationGenerateAsyncResponse + +__all__ = ["ApplicationJobsListResponse", "Pagination"] + + +class Pagination(BaseModel): + limit: Optional[int] = None + """The pagination limit for retrieving the jobs.""" + + offset: Optional[int] = None + """The pagination offset for retrieving the jobs.""" + + +class ApplicationJobsListResponse(BaseModel): + result: List[ApplicationGenerateAsyncResponse] + + pagination: Optional[Pagination] = None + + total_count: Optional[int] = FieldInfo(alias="totalCount", default=None) + """The total number of jobs associated with the application.""" diff --git a/src/writerai/types/applications/job_retrieve_response.py b/src/writerai/types/applications/job_retrieve_response.py deleted file mode 100644 index 8de6ad3e..00000000 --- a/src/writerai/types/applications/job_retrieve_response.py +++ /dev/null @@ -1,36 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional -from datetime import datetime -from typing_extensions import Literal - -from ..._models import BaseModel -from ..application_generate_content_response import ApplicationGenerateContentResponse - -__all__ = ["JobRetrieveResponse"] - - -class JobRetrieveResponse(BaseModel): - id: str - """The unique identifier for the job.""" - - application_id: str - """The ID of the application associated with this job.""" - - created_at: datetime - """The timestamp when the job was created.""" - - status: Literal["in_progress", "failed", "completed"] - """The status of the job.""" - - completed_at: Optional[datetime] = None - """The timestamp when the job was completed.""" - - data: Optional[ApplicationGenerateContentResponse] = None - """The result of the completed job, if applicable.""" - - error: Optional[str] = None - """The error message if the job failed.""" - - updated_at: Optional[datetime] = None - """The timestamp when the job was last updated.""" diff --git a/tests/api_resources/applications/test_jobs.py b/tests/api_resources/applications/test_jobs.py index 96a5db3c..57eba30f 100644 --- a/tests/api_resources/applications/test_jobs.py +++ b/tests/api_resources/applications/test_jobs.py @@ -11,10 +11,9 @@ from tests.utils import assert_matches_type from writerai.pagination import SyncApplicationJobsOffset, AsyncApplicationJobsOffset from writerai.types.applications import ( - JobListResponse, JobRetryResponse, JobCreateResponse, - JobRetrieveResponse, + ApplicationGenerateAsyncResponse, ) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -90,7 +89,7 @@ def test_method_retrieve(self, client: Writer) -> None: job = client.applications.jobs.retrieve( "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) - assert_matches_type(JobRetrieveResponse, job, path=["response"]) + assert_matches_type(ApplicationGenerateAsyncResponse, job, path=["response"]) @parametrize def test_raw_response_retrieve(self, client: Writer) -> None: @@ -101,7 +100,7 @@ def test_raw_response_retrieve(self, client: Writer) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" job = response.parse() - assert_matches_type(JobRetrieveResponse, job, path=["response"]) + assert_matches_type(ApplicationGenerateAsyncResponse, job, path=["response"]) @parametrize def test_streaming_response_retrieve(self, client: Writer) -> None: @@ -112,7 +111,7 @@ def test_streaming_response_retrieve(self, client: Writer) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" job = response.parse() - assert_matches_type(JobRetrieveResponse, job, path=["response"]) + assert_matches_type(ApplicationGenerateAsyncResponse, job, path=["response"]) assert cast(Any, response.is_closed) is True @@ -128,7 +127,7 @@ def test_method_list(self, client: Writer) -> None: job = client.applications.jobs.list( application_id="application_id", ) - assert_matches_type(SyncApplicationJobsOffset[JobListResponse], job, path=["response"]) + assert_matches_type(SyncApplicationJobsOffset[ApplicationGenerateAsyncResponse], job, path=["response"]) @parametrize def test_method_list_with_all_params(self, client: Writer) -> None: @@ -138,7 +137,7 @@ def test_method_list_with_all_params(self, client: Writer) -> None: offset=0, status="in_progress", ) - assert_matches_type(SyncApplicationJobsOffset[JobListResponse], job, path=["response"]) + assert_matches_type(SyncApplicationJobsOffset[ApplicationGenerateAsyncResponse], job, path=["response"]) @parametrize def test_raw_response_list(self, client: Writer) -> None: @@ -149,7 +148,7 @@ def test_raw_response_list(self, client: Writer) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" job = response.parse() - assert_matches_type(SyncApplicationJobsOffset[JobListResponse], job, path=["response"]) + assert_matches_type(SyncApplicationJobsOffset[ApplicationGenerateAsyncResponse], job, path=["response"]) @parametrize def test_streaming_response_list(self, client: Writer) -> None: @@ -160,7 +159,7 @@ def test_streaming_response_list(self, client: Writer) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" job = response.parse() - assert_matches_type(SyncApplicationJobsOffset[JobListResponse], job, path=["response"]) + assert_matches_type(SyncApplicationJobsOffset[ApplicationGenerateAsyncResponse], job, path=["response"]) assert cast(Any, response.is_closed) is True @@ -280,7 +279,7 @@ async def test_method_retrieve(self, async_client: AsyncWriter) -> None: job = await async_client.applications.jobs.retrieve( "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) - assert_matches_type(JobRetrieveResponse, job, path=["response"]) + assert_matches_type(ApplicationGenerateAsyncResponse, job, path=["response"]) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncWriter) -> None: @@ -291,7 +290,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncWriter) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" job = await response.parse() - assert_matches_type(JobRetrieveResponse, job, path=["response"]) + assert_matches_type(ApplicationGenerateAsyncResponse, job, path=["response"]) @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncWriter) -> None: @@ -302,7 +301,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncWriter) -> N assert response.http_request.headers.get("X-Stainless-Lang") == "python" job = await response.parse() - assert_matches_type(JobRetrieveResponse, job, path=["response"]) + assert_matches_type(ApplicationGenerateAsyncResponse, job, path=["response"]) assert cast(Any, response.is_closed) is True @@ -318,7 +317,7 @@ async def test_method_list(self, async_client: AsyncWriter) -> None: job = await async_client.applications.jobs.list( application_id="application_id", ) - assert_matches_type(AsyncApplicationJobsOffset[JobListResponse], job, path=["response"]) + assert_matches_type(AsyncApplicationJobsOffset[ApplicationGenerateAsyncResponse], job, path=["response"]) @parametrize async def test_method_list_with_all_params(self, async_client: AsyncWriter) -> None: @@ -328,7 +327,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncWriter) -> N offset=0, status="in_progress", ) - assert_matches_type(AsyncApplicationJobsOffset[JobListResponse], job, path=["response"]) + assert_matches_type(AsyncApplicationJobsOffset[ApplicationGenerateAsyncResponse], job, path=["response"]) @parametrize async def test_raw_response_list(self, async_client: AsyncWriter) -> None: @@ -339,7 +338,7 @@ async def test_raw_response_list(self, async_client: AsyncWriter) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" job = await response.parse() - assert_matches_type(AsyncApplicationJobsOffset[JobListResponse], job, path=["response"]) + assert_matches_type(AsyncApplicationJobsOffset[ApplicationGenerateAsyncResponse], job, path=["response"]) @parametrize async def test_streaming_response_list(self, async_client: AsyncWriter) -> None: @@ -350,7 +349,7 @@ async def test_streaming_response_list(self, async_client: AsyncWriter) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" job = await response.parse() - assert_matches_type(AsyncApplicationJobsOffset[JobListResponse], job, path=["response"]) + assert_matches_type(AsyncApplicationJobsOffset[ApplicationGenerateAsyncResponse], job, path=["response"]) assert cast(Any, response.is_closed) is True From 0660eb64e1fc5f353a7bdaabef3300d45ba22378 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 30 Jan 2025 22:42:54 +0000 Subject: [PATCH 167/399] chore(api): fixes to ApplicationJobs pagination (#172) --- src/writerai/pagination.py | 39 +++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/src/writerai/pagination.py b/src/writerai/pagination.py index 6a634195..0478bd36 100644 --- a/src/writerai/pagination.py +++ b/src/writerai/pagination.py @@ -3,9 +3,18 @@ from typing import Any, List, Generic, TypeVar, Optional, cast from typing_extensions import Protocol, override, runtime_checkable +from pydantic import Field as FieldInfo + +from ._models import BaseModel from ._base_client import BasePage, PageInfo, BaseSyncPage, BaseAsyncPage -__all__ = ["SyncCursorPage", "AsyncCursorPage", "SyncApplicationJobsOffset", "AsyncApplicationJobsOffset"] +__all__ = [ + "SyncCursorPage", + "AsyncCursorPage", + "ApplicationJobsOffsetPagination", + "SyncApplicationJobsOffset", + "AsyncApplicationJobsOffset", +] _T = TypeVar("_T") @@ -85,8 +94,16 @@ def next_page_info(self) -> Optional[PageInfo]: return PageInfo(params={"before": item.id}) +class ApplicationJobsOffsetPagination(BaseModel): + limit: Optional[int] = None + + offset: Optional[int] = None + + class SyncApplicationJobsOffset(BaseSyncPage[_T], BasePage[_T], Generic[_T]): result: List[_T] + total_count: Optional[int] = FieldInfo(alias="totalCount", default=None) + pagination: Optional[ApplicationJobsOffsetPagination] = None @override def _get_page_items(self) -> List[_T]: @@ -104,11 +121,20 @@ def next_page_info(self) -> Optional[PageInfo]: length = len(self._get_page_items()) current_count = offset + length - return PageInfo(params={"offset": current_count}) + total_count = self.total_count + if total_count is None: + return None + + if current_count < total_count: + return PageInfo(params={"offset": current_count}) + + return None class AsyncApplicationJobsOffset(BaseAsyncPage[_T], BasePage[_T], Generic[_T]): result: List[_T] + total_count: Optional[int] = FieldInfo(alias="totalCount", default=None) + pagination: Optional[ApplicationJobsOffsetPagination] = None @override def _get_page_items(self) -> List[_T]: @@ -126,4 +152,11 @@ def next_page_info(self) -> Optional[PageInfo]: length = len(self._get_page_items()) current_count = offset + length - return PageInfo(params={"offset": current_count}) + total_count = self.total_count + if total_count is None: + return None + + if current_count < total_count: + return PageInfo(params={"offset": current_count}) + + return None From 5cb71e906fea9cd617f7e7c6ce0662594cd92c78 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 3 Feb 2025 14:41:01 +0000 Subject: [PATCH 168/399] chore(internal): change default timeout to an int (#173) --- src/writerai/_constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/writerai/_constants.py b/src/writerai/_constants.py index fb50e2c8..eeeb0942 100644 --- a/src/writerai/_constants.py +++ b/src/writerai/_constants.py @@ -6,7 +6,7 @@ OVERRIDE_CAST_TO_HEADER = "____stainless_override_cast_to" # default timeout is 3 minutes -DEFAULT_TIMEOUT = httpx.Timeout(timeout=180.0, connect=5.0) +DEFAULT_TIMEOUT = httpx.Timeout(timeout=180, connect=5.0) DEFAULT_MAX_RETRIES = 2 DEFAULT_CONNECTION_LIMITS = httpx.Limits(max_connections=100, max_keepalive_connections=20) From 964ab2f9d775fc20fe01cf0874c8a36a3948424f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 3 Feb 2025 15:11:04 +0000 Subject: [PATCH 169/399] chore(internal): bummp ruff dependency (#174) --- pyproject.toml | 2 +- requirements-dev.lock | 2 +- scripts/utils/ruffen-docs.py | 4 ++-- src/writerai/_models.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ae8aac05..91d50569 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -177,7 +177,7 @@ select = [ "T201", "T203", # misuse of typing.TYPE_CHECKING - "TCH004", + "TC004", # import rules "TID251", ] diff --git a/requirements-dev.lock b/requirements-dev.lock index 2c46afe5..21f1395b 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -78,7 +78,7 @@ pytz==2023.3.post1 # via dirty-equals respx==0.22.0 rich==13.7.1 -ruff==0.6.9 +ruff==0.9.4 setuptools==68.2.2 # via nodeenv six==1.16.0 diff --git a/scripts/utils/ruffen-docs.py b/scripts/utils/ruffen-docs.py index 37b3d94f..0cf2bd2f 100644 --- a/scripts/utils/ruffen-docs.py +++ b/scripts/utils/ruffen-docs.py @@ -47,7 +47,7 @@ def _md_match(match: Match[str]) -> str: with _collect_error(match): code = format_code_block(code) code = textwrap.indent(code, match["indent"]) - return f'{match["before"]}{code}{match["after"]}' + return f"{match['before']}{code}{match['after']}" def _pycon_match(match: Match[str]) -> str: code = "" @@ -97,7 +97,7 @@ def finish_fragment() -> None: def _md_pycon_match(match: Match[str]) -> str: code = _pycon_match(match) code = textwrap.indent(code, match["indent"]) - return f'{match["before"]}{code}{match["after"]}' + return f"{match['before']}{code}{match['after']}" src = MD_RE.sub(_md_match, src) src = MD_PYCON_RE.sub(_md_pycon_match, src) diff --git a/src/writerai/_models.py b/src/writerai/_models.py index 9a918aab..12c34b7d 100644 --- a/src/writerai/_models.py +++ b/src/writerai/_models.py @@ -172,7 +172,7 @@ def to_json( @override def __str__(self) -> str: # mypy complains about an invalid self arg - return f'{self.__repr_name__()}({self.__repr_str__(", ")})' # type: ignore[misc] + return f"{self.__repr_name__()}({self.__repr_str__(', ')})" # type: ignore[misc] # Override the 'construct' method in a way that supports recursive parsing without validation. # Based on https://github.com/samuelcolvin/pydantic/issues/1168#issuecomment-817742836. From 5fe396904f358a048a8fa719793f054a037d8cba Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 5 Feb 2025 17:12:32 +0000 Subject: [PATCH 170/399] feat(client): send `X-Stainless-Read-Timeout` header (#175) --- src/writerai/_base_client.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index 3f0c30da..c67010f7 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -418,10 +418,17 @@ def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0 if idempotency_header and options.method.lower() != "get" and idempotency_header not in headers: headers[idempotency_header] = options.idempotency_key or self._idempotency_key() - # Don't set the retry count header if it was already set or removed by the caller. We check + # Don't set these headers if they were already set or removed by the caller. We check # `custom_headers`, which can contain `Omit()`, instead of `headers` to account for the removal case. - if "x-stainless-retry-count" not in (header.lower() for header in custom_headers): + lower_custom_headers = [header.lower() for header in custom_headers] + if "x-stainless-retry-count" not in lower_custom_headers: headers["x-stainless-retry-count"] = str(retries_taken) + if "x-stainless-read-timeout" not in lower_custom_headers: + timeout = self.timeout if isinstance(options.timeout, NotGiven) else options.timeout + if isinstance(timeout, Timeout): + timeout = timeout.read + if timeout is not None: + headers["x-stainless-read-timeout"] = str(timeout) return headers From f64feda7fc00abf6cb21829883bd22cc44ccda21 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 5 Feb 2025 18:44:53 +0000 Subject: [PATCH 171/399] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 6f4a8723..db59f718 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 27 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-ffd1e266059dd5eabc5a0a437bbf4c7b06d497152e076a91e0cc25243a485344.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-a265074214d9160fb294f271cc655daaf3981cfc583bc41dad1653c1d7ebef9b.yml From ee5ca6c4e02c7e101278635973459de4eb642ba7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 5 Feb 2025 20:06:08 +0000 Subject: [PATCH 172/399] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index db59f718..5a9e2ba3 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 27 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-a265074214d9160fb294f271cc655daaf3981cfc583bc41dad1653c1d7ebef9b.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-0a3ffce61cb0f39390e9affbed415ac096d383b69bf143b476aae7495071d619.yml From c4a08b1e9a8c81651738f063147b0a68bf7c3afb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 5 Feb 2025 20:18:55 +0000 Subject: [PATCH 173/399] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 5a9e2ba3..8983e620 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 27 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-0a3ffce61cb0f39390e9affbed415ac096d383b69bf143b476aae7495071d619.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-6a4042c053accc87fd10b42e19e8ebe27b81127cf6ff8aa246d7f30bc6b8776f.yml From 1aa41d802915aa45b78c3916220869341601d3b6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 5 Feb 2025 20:36:05 +0000 Subject: [PATCH 174/399] feat(api): add list and retrieve applications (#176) --- .stats.yml | 2 +- api.md | 9 +- .../resources/applications/applications.py | 219 +++++++++++++++++- src/writerai/types/__init__.py | 3 + src/writerai/types/application_list_params.py | 24 ++ .../types/application_list_response.py | 103 ++++++++ .../types/application_retrieve_response.py | 103 ++++++++ tests/api_resources/test_applications.py | 155 ++++++++++++- 8 files changed, 613 insertions(+), 5 deletions(-) create mode 100644 src/writerai/types/application_list_params.py create mode 100644 src/writerai/types/application_list_response.py create mode 100644 src/writerai/types/application_retrieve_response.py diff --git a/.stats.yml b/.stats.yml index 8983e620..322588bd 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ -configured_endpoints: 27 +configured_endpoints: 29 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-6a4042c053accc87fd10b42e19e8ebe27b81127cf6ff8aa246d7f30bc6b8776f.yml diff --git a/api.md b/api.md index 44d29873..2749ea06 100644 --- a/api.md +++ b/api.md @@ -23,11 +23,18 @@ from writerai.types import ( Types: ```python -from writerai.types import ApplicationGenerateContentChunk, ApplicationGenerateContentResponse +from writerai.types import ( + ApplicationGenerateContentChunk, + ApplicationGenerateContentResponse, + ApplicationRetrieveResponse, + ApplicationListResponse, +) ``` Methods: +- client.applications.retrieve(application_id) -> ApplicationRetrieveResponse +- client.applications.list(\*\*params) -> SyncCursorPage[ApplicationListResponse] - client.applications.generate_content(application_id, \*\*params) -> ApplicationGenerateContentResponse ## Jobs diff --git a/src/writerai/resources/applications/applications.py b/src/writerai/resources/applications/applications.py index 8251b1f5..06a8028a 100644 --- a/src/writerai/resources/applications/applications.py +++ b/src/writerai/resources/applications/applications.py @@ -23,7 +23,7 @@ GraphsResourceWithStreamingResponse, AsyncGraphsResourceWithStreamingResponse, ) -from ...types import application_generate_content_params +from ...types import application_list_params, application_generate_content_params from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven from ..._utils import ( required_args, @@ -39,7 +39,10 @@ async_to_streamed_response_wrapper, ) from ..._streaming import Stream, AsyncStream -from ..._base_client import make_request_options +from ...pagination import SyncCursorPage, AsyncCursorPage +from ..._base_client import AsyncPaginator, make_request_options +from ...types.application_list_response import ApplicationListResponse +from ...types.application_retrieve_response import ApplicationRetrieveResponse from ...types.application_generate_content_chunk import ApplicationGenerateContentChunk from ...types.application_generate_content_response import ApplicationGenerateContentResponse @@ -74,6 +77,100 @@ def with_streaming_response(self) -> ApplicationsResourceWithStreamingResponse: """ return ApplicationsResourceWithStreamingResponse(self) + def retrieve( + self, + application_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ApplicationRetrieveResponse: + """ + Retrieves detailed information for a specific no-code application, including its + configuration and current status. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not application_id: + raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") + return self._get( + f"/v1/applications/{application_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ApplicationRetrieveResponse, + ) + + def list( + self, + *, + after: str | NotGiven = NOT_GIVEN, + before: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, + type: Literal["generation"] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncCursorPage[ApplicationListResponse]: + """ + Retrieves a paginated list of no-code applications with optional filtering and + sorting capabilities. + + Args: + after: Return results after this application ID for pagination. + + before: Return results before this application ID for pagination. + + limit: Maximum number of applications to return in the response. + + order: Sort order for the results based on creation time. + + type: Filter applications by their type. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/applications", + page=SyncCursorPage[ApplicationListResponse], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "after": after, + "before": before, + "limit": limit, + "order": order, + "type": type, + }, + application_list_params.ApplicationListParams, + ), + ), + model=ApplicationListResponse, + ) + @overload def generate_content( self, @@ -229,6 +326,100 @@ def with_streaming_response(self) -> AsyncApplicationsResourceWithStreamingRespo """ return AsyncApplicationsResourceWithStreamingResponse(self) + async def retrieve( + self, + application_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ApplicationRetrieveResponse: + """ + Retrieves detailed information for a specific no-code application, including its + configuration and current status. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not application_id: + raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") + return await self._get( + f"/v1/applications/{application_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ApplicationRetrieveResponse, + ) + + def list( + self, + *, + after: str | NotGiven = NOT_GIVEN, + before: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, + type: Literal["generation"] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[ApplicationListResponse, AsyncCursorPage[ApplicationListResponse]]: + """ + Retrieves a paginated list of no-code applications with optional filtering and + sorting capabilities. + + Args: + after: Return results after this application ID for pagination. + + before: Return results before this application ID for pagination. + + limit: Maximum number of applications to return in the response. + + order: Sort order for the results based on creation time. + + type: Filter applications by their type. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/applications", + page=AsyncCursorPage[ApplicationListResponse], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "after": after, + "before": before, + "limit": limit, + "order": order, + "type": type, + }, + application_list_params.ApplicationListParams, + ), + ), + model=ApplicationListResponse, + ) + @overload async def generate_content( self, @@ -360,6 +551,12 @@ class ApplicationsResourceWithRawResponse: def __init__(self, applications: ApplicationsResource) -> None: self._applications = applications + self.retrieve = to_raw_response_wrapper( + applications.retrieve, + ) + self.list = to_raw_response_wrapper( + applications.list, + ) self.generate_content = to_raw_response_wrapper( applications.generate_content, ) @@ -377,6 +574,12 @@ class AsyncApplicationsResourceWithRawResponse: def __init__(self, applications: AsyncApplicationsResource) -> None: self._applications = applications + self.retrieve = async_to_raw_response_wrapper( + applications.retrieve, + ) + self.list = async_to_raw_response_wrapper( + applications.list, + ) self.generate_content = async_to_raw_response_wrapper( applications.generate_content, ) @@ -394,6 +597,12 @@ class ApplicationsResourceWithStreamingResponse: def __init__(self, applications: ApplicationsResource) -> None: self._applications = applications + self.retrieve = to_streamed_response_wrapper( + applications.retrieve, + ) + self.list = to_streamed_response_wrapper( + applications.list, + ) self.generate_content = to_streamed_response_wrapper( applications.generate_content, ) @@ -411,6 +620,12 @@ class AsyncApplicationsResourceWithStreamingResponse: def __init__(self, applications: AsyncApplicationsResource) -> None: self._applications = applications + self.retrieve = async_to_streamed_response_wrapper( + applications.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + applications.list, + ) self.generate_content = async_to_streamed_response_wrapper( applications.generate_content, ) diff --git a/src/writerai/types/__init__.py b/src/writerai/types/__init__.py index 8e782bc2..e76f9b35 100644 --- a/src/writerai/types/__init__.py +++ b/src/writerai/types/__init__.py @@ -41,10 +41,13 @@ from .graph_update_response import GraphUpdateResponse as GraphUpdateResponse from .tool_parse_pdf_params import ToolParsePdfParams as ToolParsePdfParams from .chat_completion_choice import ChatCompletionChoice as ChatCompletionChoice +from .application_list_params import ApplicationListParams as ApplicationListParams from .chat_completion_message import ChatCompletionMessage as ChatCompletionMessage from .question_response_chunk import QuestionResponseChunk as QuestionResponseChunk from .tool_parse_pdf_response import ToolParsePdfResponse as ToolParsePdfResponse from .completion_create_params import CompletionCreateParams as CompletionCreateParams +from .application_list_response import ApplicationListResponse as ApplicationListResponse +from .application_retrieve_response import ApplicationRetrieveResponse as ApplicationRetrieveResponse from .graph_add_file_to_graph_params import GraphAddFileToGraphParams as GraphAddFileToGraphParams from .application_generate_content_chunk import ApplicationGenerateContentChunk as ApplicationGenerateContentChunk from .application_generate_content_params import ApplicationGenerateContentParams as ApplicationGenerateContentParams diff --git a/src/writerai/types/application_list_params.py b/src/writerai/types/application_list_params.py new file mode 100644 index 00000000..1753e83b --- /dev/null +++ b/src/writerai/types/application_list_params.py @@ -0,0 +1,24 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, TypedDict + +__all__ = ["ApplicationListParams"] + + +class ApplicationListParams(TypedDict, total=False): + after: str + """Return results after this application ID for pagination.""" + + before: str + """Return results before this application ID for pagination.""" + + limit: int + """Maximum number of applications to return in the response.""" + + order: Literal["asc", "desc"] + """Sort order for the results based on creation time.""" + + type: Literal["generation"] + """Filter applications by their type.""" diff --git a/src/writerai/types/application_list_response.py b/src/writerai/types/application_list_response.py new file mode 100644 index 00000000..2c1228ba --- /dev/null +++ b/src/writerai/types/application_list_response.py @@ -0,0 +1,103 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Union, Optional +from datetime import datetime +from typing_extensions import Literal, TypeAlias + +from .._models import BaseModel + +__all__ = [ + "ApplicationListResponse", + "Input", + "InputOptions", + "InputOptionsApplicationInputDropdownOptions", + "InputOptionsApplicationInputFileOptions", + "InputOptionsApplicationInputMediaOptions", + "InputOptionsApplicationInputTextOptions", +] + + +class InputOptionsApplicationInputDropdownOptions(BaseModel): + list: List[str] + """List of available options in the dropdown menu.""" + + +class InputOptionsApplicationInputFileOptions(BaseModel): + file_types: List[str] + """List of allowed file extensions.""" + + max_file_size_mb: int + """Maximum file size allowed in megabytes.""" + + max_files: int + """Maximum number of files that can be uploaded.""" + + max_word_count: int + """Maximum number of words allowed in text files.""" + + +class InputOptionsApplicationInputMediaOptions(BaseModel): + file_types: List[str] + """List of allowed media file types.""" + + max_image_size_mb: int + """Maximum media file size allowed in megabytes.""" + + +class InputOptionsApplicationInputTextOptions(BaseModel): + max_fields: int + """Maximum number of text fields allowed.""" + + min_fields: int + """Minimum number of text fields required.""" + + +InputOptions: TypeAlias = Union[ + InputOptionsApplicationInputDropdownOptions, + InputOptionsApplicationInputFileOptions, + InputOptionsApplicationInputMediaOptions, + InputOptionsApplicationInputTextOptions, +] + + +class Input(BaseModel): + input_type: Literal["text", "dropdown", "file", "media"] + """Type of input field determining its behavior and validation rules.""" + + name: str + """Identifier for the input field.""" + + required: bool + """Indicates if this input field is mandatory.""" + + description: Optional[str] = None + """Human-readable description of the input field's purpose.""" + + options: Optional[InputOptions] = None + """Type-specific configuration options for input fields.""" + + +class ApplicationListResponse(BaseModel): + id: str + """Unique identifier for the application.""" + + created_at: datetime + """Timestamp when the application was created.""" + + inputs: List[Input] + """List of input configurations for the application.""" + + name: str + """Display name of the application.""" + + status: Literal["deployed", "draft"] + """Current deployment status of the application.""" + + type: Literal["generation"] + """The type of no-code application.""" + + updated_at: datetime + """Timestamp when the application was last updated.""" + + last_deployed_at: Optional[datetime] = None + """Timestamp when the application was last deployed.""" diff --git a/src/writerai/types/application_retrieve_response.py b/src/writerai/types/application_retrieve_response.py new file mode 100644 index 00000000..52d06277 --- /dev/null +++ b/src/writerai/types/application_retrieve_response.py @@ -0,0 +1,103 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Union, Optional +from datetime import datetime +from typing_extensions import Literal, TypeAlias + +from .._models import BaseModel + +__all__ = [ + "ApplicationRetrieveResponse", + "Input", + "InputOptions", + "InputOptionsApplicationInputDropdownOptions", + "InputOptionsApplicationInputFileOptions", + "InputOptionsApplicationInputMediaOptions", + "InputOptionsApplicationInputTextOptions", +] + + +class InputOptionsApplicationInputDropdownOptions(BaseModel): + list: List[str] + """List of available options in the dropdown menu.""" + + +class InputOptionsApplicationInputFileOptions(BaseModel): + file_types: List[str] + """List of allowed file extensions.""" + + max_file_size_mb: int + """Maximum file size allowed in megabytes.""" + + max_files: int + """Maximum number of files that can be uploaded.""" + + max_word_count: int + """Maximum number of words allowed in text files.""" + + +class InputOptionsApplicationInputMediaOptions(BaseModel): + file_types: List[str] + """List of allowed media file types.""" + + max_image_size_mb: int + """Maximum media file size allowed in megabytes.""" + + +class InputOptionsApplicationInputTextOptions(BaseModel): + max_fields: int + """Maximum number of text fields allowed.""" + + min_fields: int + """Minimum number of text fields required.""" + + +InputOptions: TypeAlias = Union[ + InputOptionsApplicationInputDropdownOptions, + InputOptionsApplicationInputFileOptions, + InputOptionsApplicationInputMediaOptions, + InputOptionsApplicationInputTextOptions, +] + + +class Input(BaseModel): + input_type: Literal["text", "dropdown", "file", "media"] + """Type of input field determining its behavior and validation rules.""" + + name: str + """Identifier for the input field.""" + + required: bool + """Indicates if this input field is mandatory.""" + + description: Optional[str] = None + """Human-readable description of the input field's purpose.""" + + options: Optional[InputOptions] = None + """Type-specific configuration options for input fields.""" + + +class ApplicationRetrieveResponse(BaseModel): + id: str + """Unique identifier for the application.""" + + created_at: datetime + """Timestamp when the application was created.""" + + inputs: List[Input] + """List of input configurations for the application.""" + + name: str + """Display name of the application.""" + + status: Literal["deployed", "draft"] + """Current deployment status of the application.""" + + type: Literal["generation"] + """The type of no-code application.""" + + updated_at: datetime + """Timestamp when the application was last updated.""" + + last_deployed_at: Optional[datetime] = None + """Timestamp when the application was last deployed.""" diff --git a/tests/api_resources/test_applications.py b/tests/api_resources/test_applications.py index 6d5e45af..86678535 100644 --- a/tests/api_resources/test_applications.py +++ b/tests/api_resources/test_applications.py @@ -9,7 +9,12 @@ from writerai import Writer, AsyncWriter from tests.utils import assert_matches_type -from writerai.types import ApplicationGenerateContentResponse +from writerai.types import ( + ApplicationListResponse, + ApplicationRetrieveResponse, + ApplicationGenerateContentResponse, +) +from writerai.pagination import SyncCursorPage, AsyncCursorPage base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -17,6 +22,80 @@ class TestApplications: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + @parametrize + def test_method_retrieve(self, client: Writer) -> None: + application = client.applications.retrieve( + "application_id", + ) + assert_matches_type(ApplicationRetrieveResponse, application, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Writer) -> None: + response = client.applications.with_raw_response.retrieve( + "application_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + application = response.parse() + assert_matches_type(ApplicationRetrieveResponse, application, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Writer) -> None: + with client.applications.with_streaming_response.retrieve( + "application_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + application = response.parse() + assert_matches_type(ApplicationRetrieveResponse, application, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Writer) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `application_id` but received ''"): + client.applications.with_raw_response.retrieve( + "", + ) + + @parametrize + def test_method_list(self, client: Writer) -> None: + application = client.applications.list() + assert_matches_type(SyncCursorPage[ApplicationListResponse], application, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Writer) -> None: + application = client.applications.list( + after="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + before="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + limit=0, + order="asc", + type="generation", + ) + assert_matches_type(SyncCursorPage[ApplicationListResponse], application, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Writer) -> None: + response = client.applications.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + application = response.parse() + assert_matches_type(SyncCursorPage[ApplicationListResponse], application, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Writer) -> None: + with client.applications.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + application = response.parse() + assert_matches_type(SyncCursorPage[ApplicationListResponse], application, path=["response"]) + + assert cast(Any, response.is_closed) is True + @parametrize def test_method_generate_content_overload_1(self, client: Writer) -> None: application = client.applications.generate_content( @@ -162,6 +241,80 @@ def test_path_params_generate_content_overload_2(self, client: Writer) -> None: class TestAsyncApplications: parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + @parametrize + async def test_method_retrieve(self, async_client: AsyncWriter) -> None: + application = await async_client.applications.retrieve( + "application_id", + ) + assert_matches_type(ApplicationRetrieveResponse, application, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncWriter) -> None: + response = await async_client.applications.with_raw_response.retrieve( + "application_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + application = await response.parse() + assert_matches_type(ApplicationRetrieveResponse, application, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncWriter) -> None: + async with async_client.applications.with_streaming_response.retrieve( + "application_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + application = await response.parse() + assert_matches_type(ApplicationRetrieveResponse, application, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncWriter) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `application_id` but received ''"): + await async_client.applications.with_raw_response.retrieve( + "", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncWriter) -> None: + application = await async_client.applications.list() + assert_matches_type(AsyncCursorPage[ApplicationListResponse], application, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncWriter) -> None: + application = await async_client.applications.list( + after="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + before="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + limit=0, + order="asc", + type="generation", + ) + assert_matches_type(AsyncCursorPage[ApplicationListResponse], application, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncWriter) -> None: + response = await async_client.applications.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + application = await response.parse() + assert_matches_type(AsyncCursorPage[ApplicationListResponse], application, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncWriter) -> None: + async with async_client.applications.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + application = await response.parse() + assert_matches_type(AsyncCursorPage[ApplicationListResponse], application, path=["response"]) + + assert cast(Any, response.is_closed) is True + @parametrize async def test_method_generate_content_overload_1(self, async_client: AsyncWriter) -> None: application = await async_client.applications.generate_content( From 3b358100f26f45009c968840b41fb1923da05ab8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 5 Feb 2025 20:45:51 +0000 Subject: [PATCH 175/399] fix(api): fix offset pagination schema (#177) --- src/writerai/pagination.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/writerai/pagination.py b/src/writerai/pagination.py index 0478bd36..411b2ef1 100644 --- a/src/writerai/pagination.py +++ b/src/writerai/pagination.py @@ -114,9 +114,12 @@ def _get_page_items(self) -> List[_T]: @override def next_page_info(self) -> Optional[PageInfo]: - offset = self._options.params.get("offset") or 0 - if not isinstance(offset, int): - raise ValueError(f'Expected "offset" param to be an integer but got {offset}') + offset = None + if self.pagination is not None: + if self.pagination.offset is not None: + offset = self.pagination.offset + if offset is None: + return None length = len(self._get_page_items()) current_count = offset + length @@ -145,9 +148,12 @@ def _get_page_items(self) -> List[_T]: @override def next_page_info(self) -> Optional[PageInfo]: - offset = self._options.params.get("offset") or 0 - if not isinstance(offset, int): - raise ValueError(f'Expected "offset" param to be an integer but got {offset}') + offset = None + if self.pagination is not None: + if self.pagination.offset is not None: + offset = self.pagination.offset + if offset is None: + return None length = len(self._get_page_items()) current_count = offset + length From ddde9aab86e13bd7133edfa83148c276c70880c9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 6 Feb 2025 12:55:15 +0000 Subject: [PATCH 176/399] chore(internal): fix type traversing dictionary params (#178) --- src/writerai/_utils/_transform.py | 12 +++++++++++- tests/test_transform.py | 11 ++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/writerai/_utils/_transform.py b/src/writerai/_utils/_transform.py index a6b62cad..18afd9d8 100644 --- a/src/writerai/_utils/_transform.py +++ b/src/writerai/_utils/_transform.py @@ -25,7 +25,7 @@ is_annotated_type, strip_annotated_type, ) -from .._compat import model_dump, is_typeddict +from .._compat import get_origin, model_dump, is_typeddict _T = TypeVar("_T") @@ -164,9 +164,14 @@ def _transform_recursive( inner_type = annotation stripped_type = strip_annotated_type(inner_type) + origin = get_origin(stripped_type) or stripped_type if is_typeddict(stripped_type) and is_mapping(data): return _transform_typeddict(data, stripped_type) + if origin == dict and is_mapping(data): + items_type = get_args(stripped_type)[1] + return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()} + if ( # List[T] (is_list_type(stripped_type) and is_list(data)) @@ -307,9 +312,14 @@ async def _async_transform_recursive( inner_type = annotation stripped_type = strip_annotated_type(inner_type) + origin = get_origin(stripped_type) or stripped_type if is_typeddict(stripped_type) and is_mapping(data): return await _async_transform_typeddict(data, stripped_type) + if origin == dict and is_mapping(data): + items_type = get_args(stripped_type)[1] + return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()} + if ( # List[T] (is_list_type(stripped_type) and is_list(data)) diff --git a/tests/test_transform.py b/tests/test_transform.py index 2c1448c5..07bc24e8 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -2,7 +2,7 @@ import io import pathlib -from typing import Any, List, Union, TypeVar, Iterable, Optional, cast +from typing import Any, Dict, List, Union, TypeVar, Iterable, Optional, cast from datetime import date, datetime from typing_extensions import Required, Annotated, TypedDict @@ -388,6 +388,15 @@ def my_iter() -> Iterable[Baz8]: } +@parametrize +@pytest.mark.asyncio +async def test_dictionary_items(use_async: bool) -> None: + class DictItems(TypedDict): + foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")] + + assert await transform({"foo": {"foo_baz": "bar"}}, Dict[str, DictItems], use_async) == {"foo": {"fooBaz": "bar"}} + + class TypedDictIterableUnionStr(TypedDict): foo: Annotated[Union[str, Iterable[Baz8]], PropertyInfo(alias="FOO")] From b0bfde6124e6008230c7bba4d2e45e36120ac6e3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 6 Feb 2025 14:46:58 +0000 Subject: [PATCH 177/399] chore(internal): codegen related update (#179) --- src/writerai/pagination.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/writerai/pagination.py b/src/writerai/pagination.py index 411b2ef1..a68ce8b3 100644 --- a/src/writerai/pagination.py +++ b/src/writerai/pagination.py @@ -35,6 +35,11 @@ def _get_page_items(self) -> List[_T]: return [] return data + @override + def has_next_page(self) -> bool: + has_more = self.has_more + return has_more and super().has_next_page() + @override def next_page_info(self) -> Optional[PageInfo]: is_forwards = not self._options.params.get("before", False) @@ -70,6 +75,11 @@ def _get_page_items(self) -> List[_T]: return [] return data + @override + def has_next_page(self) -> bool: + has_more = self.has_more + return has_more and super().has_next_page() + @override def next_page_info(self) -> Optional[PageInfo]: is_forwards = not self._options.params.get("before", False) From 1a074603b2875614fe66eb3b55ab12cdc5929770 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 6 Feb 2025 16:19:56 +0000 Subject: [PATCH 178/399] chore(internal): minor type handling changes (#180) --- src/writerai/_models.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/writerai/_models.py b/src/writerai/_models.py index 12c34b7d..c4401ff8 100644 --- a/src/writerai/_models.py +++ b/src/writerai/_models.py @@ -426,10 +426,16 @@ def construct_type(*, value: object, type_: object) -> object: If the given value does not match the expected type then it is returned as-is. """ + + # store a reference to the original type we were given before we extract any inner + # types so that we can properly resolve forward references in `TypeAliasType` annotations + original_type = None + # we allow `object` as the input type because otherwise, passing things like # `Literal['value']` will be reported as a type error by type checkers type_ = cast("type[object]", type_) if is_type_alias_type(type_): + original_type = type_ # type: ignore[unreachable] type_ = type_.__value__ # type: ignore[unreachable] # unwrap `Annotated[T, ...]` -> `T` @@ -446,7 +452,7 @@ def construct_type(*, value: object, type_: object) -> object: if is_union(origin): try: - return validate_type(type_=cast("type[object]", type_), value=value) + return validate_type(type_=cast("type[object]", original_type or type_), value=value) except Exception: pass From 732435719f3b79a78f6fbeb757fdf606419adf7e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 10 Feb 2025 16:41:53 +0000 Subject: [PATCH 179/399] docs(api): updates to API spec (#181) --- .stats.yml | 2 +- src/writerai/resources/chat.py | 24 +++++++++--------- .../types/application_list_response.py | 5 +++- .../types/application_retrieve_response.py | 5 +++- src/writerai/types/chat_chat_params.py | 4 +-- src/writerai/types/chat_completion_chunk.py | 12 ++++++++- src/writerai/types/chat_completion_message.py | 12 ++++++++- .../types/shared/function_definition.py | 5 ++-- src/writerai/types/shared/tool_param.py | 25 ++++++++++++++++--- .../shared_params/function_definition.py | 5 ++-- .../types/shared_params/tool_param.py | 22 ++++++++++++++-- 11 files changed, 93 insertions(+), 28 deletions(-) diff --git a/.stats.yml b/.stats.yml index 322588bd..d9f41df1 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 29 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-6a4042c053accc87fd10b42e19e8ebe27b81127cf6ff8aa246d7f30bc6b8776f.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-5d57be55ac24a66e8c78594a3a964a90f12f98fc51f8c758891ec7fee2b74c9f.yml diff --git a/src/writerai/resources/chat.py b/src/writerai/resources/chat.py index 5ba81333..4edd3bef 100644 --- a/src/writerai/resources/chat.py +++ b/src/writerai/resources/chat.py @@ -116,8 +116,8 @@ def chat( pass a specific previously defined function. tools: An array of tools described to the model using JSON schema that the model can - use to generate responses. Passing graph IDs will automatically use the - Knowledge Graph tool. + use to generate responses. You can define your own functions or use the built-in + `graph` or `llm` tools. top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with @@ -199,8 +199,8 @@ def chat( pass a specific previously defined function. tools: An array of tools described to the model using JSON schema that the model can - use to generate responses. Passing graph IDs will automatically use the - Knowledge Graph tool. + use to generate responses. You can define your own functions or use the built-in + `graph` or `llm` tools. top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with @@ -282,8 +282,8 @@ def chat( pass a specific previously defined function. tools: An array of tools described to the model using JSON schema that the model can - use to generate responses. Passing graph IDs will automatically use the - Knowledge Graph tool. + use to generate responses. You can define your own functions or use the built-in + `graph` or `llm` tools. top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with @@ -436,8 +436,8 @@ async def chat( pass a specific previously defined function. tools: An array of tools described to the model using JSON schema that the model can - use to generate responses. Passing graph IDs will automatically use the - Knowledge Graph tool. + use to generate responses. You can define your own functions or use the built-in + `graph` or `llm` tools. top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with @@ -519,8 +519,8 @@ async def chat( pass a specific previously defined function. tools: An array of tools described to the model using JSON schema that the model can - use to generate responses. Passing graph IDs will automatically use the - Knowledge Graph tool. + use to generate responses. You can define your own functions or use the built-in + `graph` or `llm` tools. top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with @@ -602,8 +602,8 @@ async def chat( pass a specific previously defined function. tools: An array of tools described to the model using JSON schema that the model can - use to generate responses. Passing graph IDs will automatically use the - Knowledge Graph tool. + use to generate responses. You can define your own functions or use the built-in + `graph` or `llm` tools. top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with diff --git a/src/writerai/types/application_list_response.py b/src/writerai/types/application_list_response.py index 2c1228ba..7f8e9fb0 100644 --- a/src/writerai/types/application_list_response.py +++ b/src/writerai/types/application_list_response.py @@ -91,7 +91,10 @@ class ApplicationListResponse(BaseModel): """Display name of the application.""" status: Literal["deployed", "draft"] - """Current deployment status of the application.""" + """Current deployment status of the application. + + Note: currently only `deployed` applications are returned. + """ type: Literal["generation"] """The type of no-code application.""" diff --git a/src/writerai/types/application_retrieve_response.py b/src/writerai/types/application_retrieve_response.py index 52d06277..c195b145 100644 --- a/src/writerai/types/application_retrieve_response.py +++ b/src/writerai/types/application_retrieve_response.py @@ -91,7 +91,10 @@ class ApplicationRetrieveResponse(BaseModel): """Display name of the application.""" status: Literal["deployed", "draft"] - """Current deployment status of the application.""" + """Current deployment status of the application. + + Note: currently only `deployed` applications are returned. + """ type: Literal["generation"] """The type of no-code application.""" diff --git a/src/writerai/types/chat_chat_params.py b/src/writerai/types/chat_chat_params.py index 9216e4cf..1b14328b 100644 --- a/src/writerai/types/chat_chat_params.py +++ b/src/writerai/types/chat_chat_params.py @@ -78,8 +78,8 @@ class ChatChatParamsBase(TypedDict, total=False): tools: Iterable[ToolParam] """ An array of tools described to the model using JSON schema that the model can - use to generate responses. Passing graph IDs will automatically use the - Knowledge Graph tool. + use to generate responses. You can define your own functions or use the built-in + `graph` or `llm` tools. """ top_p: float diff --git a/src/writerai/types/chat_completion_chunk.py b/src/writerai/types/chat_completion_chunk.py index 681cc663..8d9deb37 100644 --- a/src/writerai/types/chat_completion_chunk.py +++ b/src/writerai/types/chat_completion_chunk.py @@ -10,7 +10,15 @@ from .chat_completion_message import ChatCompletionMessage from .shared.tool_call_streaming import ToolCallStreaming -__all__ = ["ChatCompletionChunk", "Choice", "ChoiceDelta"] +__all__ = ["ChatCompletionChunk", "Choice", "ChoiceDelta", "ChoiceDeltaLlmData"] + + +class ChoiceDeltaLlmData(BaseModel): + model: str + """The model used by the tool.""" + + prompt: str + """The prompt processed by the model.""" class ChoiceDelta(BaseModel): @@ -23,6 +31,8 @@ class ChoiceDelta(BaseModel): graph_data: Optional[GraphData] = None + llm_data: Optional[ChoiceDeltaLlmData] = None + refusal: Optional[str] = None role: Optional[Literal["user", "assistant", "system"]] = None diff --git a/src/writerai/types/chat_completion_message.py b/src/writerai/types/chat_completion_message.py index 4187ea36..162063f7 100644 --- a/src/writerai/types/chat_completion_message.py +++ b/src/writerai/types/chat_completion_message.py @@ -7,7 +7,15 @@ from .shared.tool_call import ToolCall from .shared.graph_data import GraphData -__all__ = ["ChatCompletionMessage"] +__all__ = ["ChatCompletionMessage", "LlmData"] + + +class LlmData(BaseModel): + model: str + """The model used by the tool.""" + + prompt: str + """The prompt processed by the model.""" class ChatCompletionMessage(BaseModel): @@ -25,4 +33,6 @@ class ChatCompletionMessage(BaseModel): graph_data: Optional[GraphData] = None + llm_data: Optional[LlmData] = None + tool_calls: Optional[List[ToolCall]] = None diff --git a/src/writerai/types/shared/function_definition.py b/src/writerai/types/shared/function_definition.py index c4f1e678..357434f5 100644 --- a/src/writerai/types/shared/function_definition.py +++ b/src/writerai/types/shared/function_definition.py @@ -10,9 +10,10 @@ class FunctionDefinition(BaseModel): name: str - """Name of the function""" + """Name of the function.""" description: Optional[str] = None - """Description of the function""" + """Description of the function.""" parameters: Optional[FunctionParams] = None + """The parameters of the function.""" diff --git a/src/writerai/types/shared/tool_param.py b/src/writerai/types/shared/tool_param.py index 93b5ac25..c8761bab 100644 --- a/src/writerai/types/shared/tool_param.py +++ b/src/writerai/types/shared/tool_param.py @@ -1,16 +1,18 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import List, Union, Optional -from typing_extensions import Literal, TypeAlias +from typing_extensions import Literal, Annotated, TypeAlias +from ..._utils import PropertyInfo from ..._models import BaseModel from .function_definition import FunctionDefinition -__all__ = ["ToolParam", "FunctionTool", "GraphTool", "GraphToolFunction"] +__all__ = ["ToolParam", "FunctionTool", "GraphTool", "GraphToolFunction", "LlmTool", "LlmToolFunction"] class FunctionTool(BaseModel): function: FunctionDefinition + """A tool that uses a custom function.""" type: Literal["function"] """The type of tool.""" @@ -29,9 +31,26 @@ class GraphToolFunction(BaseModel): class GraphTool(BaseModel): function: GraphToolFunction + """A tool that uses Knowledge Graphs as context for responses.""" type: Literal["graph"] """The type of tool.""" -ToolParam: TypeAlias = Union[FunctionTool, GraphTool] +class LlmToolFunction(BaseModel): + description: str + """A description of the model to be used.""" + + model: str + """The model to be used.""" + + +class LlmTool(BaseModel): + function: LlmToolFunction + """A tool that uses another Writer model to generate a response.""" + + type: Optional[Literal["llm"]] = None + """The type of tool.""" + + +ToolParam: TypeAlias = Annotated[Union[FunctionTool, GraphTool, LlmTool], PropertyInfo(discriminator="type")] diff --git a/src/writerai/types/shared_params/function_definition.py b/src/writerai/types/shared_params/function_definition.py index 4043fa70..53606cb6 100644 --- a/src/writerai/types/shared_params/function_definition.py +++ b/src/writerai/types/shared_params/function_definition.py @@ -11,9 +11,10 @@ class FunctionDefinition(TypedDict, total=False): name: Required[str] - """Name of the function""" + """Name of the function.""" description: str - """Description of the function""" + """Description of the function.""" parameters: FunctionParams + """The parameters of the function.""" diff --git a/src/writerai/types/shared_params/tool_param.py b/src/writerai/types/shared_params/tool_param.py index 851654c1..827e0820 100644 --- a/src/writerai/types/shared_params/tool_param.py +++ b/src/writerai/types/shared_params/tool_param.py @@ -7,11 +7,12 @@ from .function_definition import FunctionDefinition -__all__ = ["ToolParam", "FunctionTool", "GraphTool", "GraphToolFunction"] +__all__ = ["ToolParam", "FunctionTool", "GraphTool", "GraphToolFunction", "LlmTool", "LlmToolFunction"] class FunctionTool(TypedDict, total=False): function: Required[FunctionDefinition] + """A tool that uses a custom function.""" type: Required[Literal["function"]] """The type of tool.""" @@ -30,9 +31,26 @@ class GraphToolFunction(TypedDict, total=False): class GraphTool(TypedDict, total=False): function: Required[GraphToolFunction] + """A tool that uses Knowledge Graphs as context for responses.""" type: Required[Literal["graph"]] """The type of tool.""" -ToolParam: TypeAlias = Union[FunctionTool, GraphTool] +class LlmToolFunction(TypedDict, total=False): + description: Required[str] + """A description of the model to be used.""" + + model: Required[str] + """The model to be used.""" + + +class LlmTool(TypedDict, total=False): + function: Required[LlmToolFunction] + """A tool that uses another Writer model to generate a response.""" + + type: Literal["llm"] + """The type of tool.""" + + +ToolParam: TypeAlias = Union[FunctionTool, GraphTool, LlmTool] From 0c92ff53e4fd49586378fa384fee85ae00d4116b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 12 Feb 2025 18:27:39 +0000 Subject: [PATCH 180/399] chore(internal): update client tests (#182) --- tests/test_client.py | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 86a3482f..2471a6e2 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -23,6 +23,7 @@ from writerai import Writer, AsyncWriter, APIResponseValidationError from writerai._types import Omit +from writerai._utils import maybe_transform from writerai._models import BaseModel, FinalRequestOptions from writerai._constants import RAW_RESPONSE_HEADER from writerai._streaming import Stream, AsyncStream @@ -33,6 +34,7 @@ BaseClient, make_request_options, ) +from writerai.types.chat_chat_params import ChatChatParamsNonStreaming from .utils import update_env @@ -727,7 +729,12 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> No with pytest.raises(APITimeoutError): self.client.post( "/v1/chat", - body=cast(object, dict(messages=[{"role": "user"}], model="palmyra-x-004")), + body=cast( + object, + maybe_transform( + dict(messages=[{"role": "user"}], model="palmyra-x-004"), ChatChatParamsNonStreaming + ), + ), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -742,7 +749,12 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> Non with pytest.raises(APIStatusError): self.client.post( "/v1/chat", - body=cast(object, dict(messages=[{"role": "user"}], model="palmyra-x-004")), + body=cast( + object, + maybe_transform( + dict(messages=[{"role": "user"}], model="palmyra-x-004"), ChatChatParamsNonStreaming + ), + ), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -1519,7 +1531,12 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) with pytest.raises(APITimeoutError): await self.client.post( "/v1/chat", - body=cast(object, dict(messages=[{"role": "user"}], model="palmyra-x-004")), + body=cast( + object, + maybe_transform( + dict(messages=[{"role": "user"}], model="palmyra-x-004"), ChatChatParamsNonStreaming + ), + ), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -1534,7 +1551,12 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) with pytest.raises(APIStatusError): await self.client.post( "/v1/chat", - body=cast(object, dict(messages=[{"role": "user"}], model="palmyra-x-004")), + body=cast( + object, + maybe_transform( + dict(messages=[{"role": "user"}], model="palmyra-x-004"), ChatChatParamsNonStreaming + ), + ), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) From 13124af557af07746fdc62b0b48279af3731f0cb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 13 Feb 2025 17:19:48 +0000 Subject: [PATCH 181/399] fix: asyncify on non-asyncio runtimes (#183) --- src/writerai/_utils/_sync.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/writerai/_utils/_sync.py b/src/writerai/_utils/_sync.py index 8b3aaf2b..ad7ec71b 100644 --- a/src/writerai/_utils/_sync.py +++ b/src/writerai/_utils/_sync.py @@ -7,16 +7,20 @@ from typing import Any, TypeVar, Callable, Awaitable from typing_extensions import ParamSpec +import anyio +import sniffio +import anyio.to_thread + T_Retval = TypeVar("T_Retval") T_ParamSpec = ParamSpec("T_ParamSpec") if sys.version_info >= (3, 9): - to_thread = asyncio.to_thread + _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 to_thread( + 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. @@ -34,6 +38,17 @@ async def to_thread( return await loop.run_in_executor(None, func_call) +async def to_thread( + func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs +) -> T_Retval: + if sniffio.current_async_library() == "asyncio": + return await _asyncio_to_thread(func, *args, **kwargs) + + return await anyio.to_thread.run_sync( + functools.partial(func, *args, **kwargs), + ) + + # inspired by `asyncer`, https://github.com/tiangolo/asyncer def asyncify(function: Callable[T_ParamSpec, T_Retval]) -> Callable[T_ParamSpec, Awaitable[T_Retval]]: """ From 7c85c5c250be38deec5b6cf88bc0e408765b0ef8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 13 Feb 2025 17:20:38 +0000 Subject: [PATCH 182/399] chore(test): update some test values (#184) --- .stats.yml | 2 +- tests/api_resources/test_chat.py | 40 ++++++++++++++++---------------- tests/test_client.py | 12 +++++----- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/.stats.yml b/.stats.yml index d9f41df1..0b428abf 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 29 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-5d57be55ac24a66e8c78594a3a964a90f12f98fc51f8c758891ec7fee2b74c9f.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-710e8a17bc916b755685592b3831e6732f3ba02904f970084aef0ac86fd79ed5.yml diff --git a/tests/api_resources/test_chat.py b/tests/api_resources/test_chat.py index f581a9d0..a1c804be 100644 --- a/tests/api_resources/test_chat.py +++ b/tests/api_resources/test_chat.py @@ -21,7 +21,7 @@ class TestChat: def test_method_chat_overload_1(self, client: Writer) -> None: chat = client.chat.chat( messages=[{"role": "user"}], - model="palmyra-x-004", + model="model", ) assert_matches_type(ChatCompletion, chat, path=["response"]) @@ -31,7 +31,7 @@ def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: messages=[ { "role": "user", - "content": "Write a memo summarizing this earnings report.", + "content": "content", "graph_data": { "sources": [ { @@ -69,7 +69,7 @@ def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: ], } ], - model="palmyra-x-004", + model="model", logprobs=True, max_tokens=0, n=0, @@ -96,7 +96,7 @@ def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: def test_raw_response_chat_overload_1(self, client: Writer) -> None: response = client.chat.with_raw_response.chat( messages=[{"role": "user"}], - model="palmyra-x-004", + model="model", ) assert response.is_closed is True @@ -108,7 +108,7 @@ def test_raw_response_chat_overload_1(self, client: Writer) -> None: def test_streaming_response_chat_overload_1(self, client: Writer) -> None: with client.chat.with_streaming_response.chat( messages=[{"role": "user"}], - model="palmyra-x-004", + model="model", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -122,7 +122,7 @@ def test_streaming_response_chat_overload_1(self, client: Writer) -> None: def test_method_chat_overload_2(self, client: Writer) -> None: chat_stream = client.chat.chat( messages=[{"role": "user"}], - model="palmyra-x-004", + model="model", stream=True, ) chat_stream.response.close() @@ -133,7 +133,7 @@ def test_method_chat_with_all_params_overload_2(self, client: Writer) -> None: messages=[ { "role": "user", - "content": "Write a memo summarizing this earnings report.", + "content": "content", "graph_data": { "sources": [ { @@ -171,7 +171,7 @@ def test_method_chat_with_all_params_overload_2(self, client: Writer) -> None: ], } ], - model="palmyra-x-004", + model="model", stream=True, logprobs=True, max_tokens=0, @@ -198,7 +198,7 @@ def test_method_chat_with_all_params_overload_2(self, client: Writer) -> None: def test_raw_response_chat_overload_2(self, client: Writer) -> None: response = client.chat.with_raw_response.chat( messages=[{"role": "user"}], - model="palmyra-x-004", + model="model", stream=True, ) @@ -210,7 +210,7 @@ def test_raw_response_chat_overload_2(self, client: Writer) -> None: def test_streaming_response_chat_overload_2(self, client: Writer) -> None: with client.chat.with_streaming_response.chat( messages=[{"role": "user"}], - model="palmyra-x-004", + model="model", stream=True, ) as response: assert not response.is_closed @@ -229,7 +229,7 @@ class TestAsyncChat: async def test_method_chat_overload_1(self, async_client: AsyncWriter) -> None: chat = await async_client.chat.chat( messages=[{"role": "user"}], - model="palmyra-x-004", + model="model", ) assert_matches_type(ChatCompletion, chat, path=["response"]) @@ -239,7 +239,7 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW messages=[ { "role": "user", - "content": "Write a memo summarizing this earnings report.", + "content": "content", "graph_data": { "sources": [ { @@ -277,7 +277,7 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW ], } ], - model="palmyra-x-004", + model="model", logprobs=True, max_tokens=0, n=0, @@ -304,7 +304,7 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW async def test_raw_response_chat_overload_1(self, async_client: AsyncWriter) -> None: response = await async_client.chat.with_raw_response.chat( messages=[{"role": "user"}], - model="palmyra-x-004", + model="model", ) assert response.is_closed is True @@ -316,7 +316,7 @@ async def test_raw_response_chat_overload_1(self, async_client: AsyncWriter) -> async def test_streaming_response_chat_overload_1(self, async_client: AsyncWriter) -> None: async with async_client.chat.with_streaming_response.chat( messages=[{"role": "user"}], - model="palmyra-x-004", + model="model", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -330,7 +330,7 @@ async def test_streaming_response_chat_overload_1(self, async_client: AsyncWrite async def test_method_chat_overload_2(self, async_client: AsyncWriter) -> None: chat_stream = await async_client.chat.chat( messages=[{"role": "user"}], - model="palmyra-x-004", + model="model", stream=True, ) await chat_stream.response.aclose() @@ -341,7 +341,7 @@ async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncW messages=[ { "role": "user", - "content": "Write a memo summarizing this earnings report.", + "content": "content", "graph_data": { "sources": [ { @@ -379,7 +379,7 @@ async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncW ], } ], - model="palmyra-x-004", + model="model", stream=True, logprobs=True, max_tokens=0, @@ -406,7 +406,7 @@ async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncW async def test_raw_response_chat_overload_2(self, async_client: AsyncWriter) -> None: response = await async_client.chat.with_raw_response.chat( messages=[{"role": "user"}], - model="palmyra-x-004", + model="model", stream=True, ) @@ -418,7 +418,7 @@ async def test_raw_response_chat_overload_2(self, async_client: AsyncWriter) -> async def test_streaming_response_chat_overload_2(self, async_client: AsyncWriter) -> None: async with async_client.chat.with_streaming_response.chat( messages=[{"role": "user"}], - model="palmyra-x-004", + model="model", stream=True, ) as response: assert not response.is_closed diff --git a/tests/test_client.py b/tests/test_client.py index 2471a6e2..2dc34c7c 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -787,7 +787,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) - response = client.chat.with_raw_response.chat(messages=[{"role": "user"}], model="palmyra-x-004") + response = client.chat.with_raw_response.chat(messages=[{"role": "user"}], model="model") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -812,7 +812,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) response = client.chat.with_raw_response.chat( - messages=[{"role": "user"}], model="palmyra-x-004", extra_headers={"x-stainless-retry-count": Omit()} + messages=[{"role": "user"}], model="model", extra_headers={"x-stainless-retry-count": Omit()} ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -837,7 +837,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) response = client.chat.with_raw_response.chat( - messages=[{"role": "user"}], model="palmyra-x-004", extra_headers={"x-stainless-retry-count": "42"} + messages=[{"role": "user"}], model="model", extra_headers={"x-stainless-retry-count": "42"} ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" @@ -1590,7 +1590,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) - response = await client.chat.with_raw_response.chat(messages=[{"role": "user"}], model="palmyra-x-004") + response = await client.chat.with_raw_response.chat(messages=[{"role": "user"}], model="model") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -1616,7 +1616,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) response = await client.chat.with_raw_response.chat( - messages=[{"role": "user"}], model="palmyra-x-004", extra_headers={"x-stainless-retry-count": Omit()} + messages=[{"role": "user"}], model="model", extra_headers={"x-stainless-retry-count": Omit()} ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -1642,7 +1642,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) response = await client.chat.with_raw_response.chat( - messages=[{"role": "user"}], model="palmyra-x-004", extra_headers={"x-stainless-retry-count": "42"} + messages=[{"role": "user"}], model="model", extra_headers={"x-stainless-retry-count": "42"} ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" From 545357a1297fb7af930d9081c53488b9fcddda2b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 14 Feb 2025 19:32:15 +0000 Subject: [PATCH 183/399] docs(api): updates to API spec (#185) --- .stats.yml | 2 +- src/writerai/resources/graphs.py | 60 ++++++++++--------- src/writerai/types/graph_create_params.py | 16 +++-- src/writerai/types/graph_update_params.py | 16 +++-- src/writerai/types/question.py | 6 +- src/writerai/types/shared/graph_data.py | 6 +- .../types/shared_params/graph_data.py | 10 ++-- tests/api_resources/test_graphs.py | 40 ++++--------- 8 files changed, 76 insertions(+), 80 deletions(-) diff --git a/.stats.yml b/.stats.yml index 0b428abf..a6d1f570 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 29 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-710e8a17bc916b755685592b3831e6732f3ba02904f970084aef0ac86fd79ed5.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-9f17b2629bd54f56bc3e48ec710b11e2ba84302725f8da9e9ed390bbed5d3b5b.yml diff --git a/src/writerai/resources/graphs.py b/src/writerai/resources/graphs.py index d7194268..7ea696a7 100644 --- a/src/writerai/resources/graphs.py +++ b/src/writerai/resources/graphs.py @@ -66,8 +66,8 @@ def with_streaming_response(self) -> GraphsResourceWithStreamingResponse: def create( self, *, - name: str, description: str | NotGiven = NOT_GIVEN, + name: str | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -75,14 +75,15 @@ def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> GraphCreateResponse: - """Create graph + """ + Create graph Args: - name: The name of the graph. - - This can be at most 255 characters. + description: A description of the graph (max 255 characters). Omitting this field leaves the + description unchanged. - description: A description of the graph. This can be at most 255 characters. + name: The name of the graph (max 255 characters). Omitting this field leaves the name + unchanged. extra_headers: Send extra headers @@ -96,8 +97,8 @@ def create( "/v1/graphs", body=maybe_transform( { - "name": name, "description": description, + "name": name, }, graph_create_params.GraphCreateParams, ), @@ -144,8 +145,8 @@ def update( self, graph_id: str, *, - name: str, description: str | NotGiven = NOT_GIVEN, + name: str | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -153,14 +154,15 @@ def update( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> GraphUpdateResponse: - """Update graph + """ + Update the name and description of a Knowledge Graph. Args: - name: The name of the graph. - - This can be at most 255 characters. + description: A description of the graph (max 255 characters). Omitting this field leaves the + description unchanged. - description: A description of the graph. This can be at most 255 characters. + name: The name of the graph (max 255 characters). Omitting this field leaves the name + unchanged. extra_headers: Send extra headers @@ -176,8 +178,8 @@ def update( f"/v1/graphs/{graph_id}", body=maybe_transform( { - "name": name, "description": description, + "name": name, }, graph_update_params.GraphUpdateParams, ), @@ -528,8 +530,8 @@ def with_streaming_response(self) -> AsyncGraphsResourceWithStreamingResponse: async def create( self, *, - name: str, description: str | NotGiven = NOT_GIVEN, + name: str | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -537,14 +539,15 @@ async def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> GraphCreateResponse: - """Create graph + """ + Create graph Args: - name: The name of the graph. - - This can be at most 255 characters. + description: A description of the graph (max 255 characters). Omitting this field leaves the + description unchanged. - description: A description of the graph. This can be at most 255 characters. + name: The name of the graph (max 255 characters). Omitting this field leaves the name + unchanged. extra_headers: Send extra headers @@ -558,8 +561,8 @@ async def create( "/v1/graphs", body=await async_maybe_transform( { - "name": name, "description": description, + "name": name, }, graph_create_params.GraphCreateParams, ), @@ -606,8 +609,8 @@ async def update( self, graph_id: str, *, - name: str, description: str | NotGiven = NOT_GIVEN, + name: str | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -615,14 +618,15 @@ async def update( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> GraphUpdateResponse: - """Update graph + """ + Update the name and description of a Knowledge Graph. Args: - name: The name of the graph. - - This can be at most 255 characters. + description: A description of the graph (max 255 characters). Omitting this field leaves the + description unchanged. - description: A description of the graph. This can be at most 255 characters. + name: The name of the graph (max 255 characters). Omitting this field leaves the name + unchanged. extra_headers: Send extra headers @@ -638,8 +642,8 @@ async def update( f"/v1/graphs/{graph_id}", body=await async_maybe_transform( { - "name": name, "description": description, + "name": name, }, graph_update_params.GraphUpdateParams, ), diff --git a/src/writerai/types/graph_create_params.py b/src/writerai/types/graph_create_params.py index 7e30204a..939e02a2 100644 --- a/src/writerai/types/graph_create_params.py +++ b/src/writerai/types/graph_create_params.py @@ -2,14 +2,20 @@ from __future__ import annotations -from typing_extensions import Required, TypedDict +from typing_extensions import TypedDict __all__ = ["GraphCreateParams"] class GraphCreateParams(TypedDict, total=False): - name: Required[str] - """The name of the graph. This can be at most 255 characters.""" - description: str - """A description of the graph. This can be at most 255 characters.""" + """A description of the graph (max 255 characters). + + Omitting this field leaves the description unchanged. + """ + + name: str + """The name of the graph (max 255 characters). + + Omitting this field leaves the name unchanged. + """ diff --git a/src/writerai/types/graph_update_params.py b/src/writerai/types/graph_update_params.py index 8c28d9e5..61f1bb52 100644 --- a/src/writerai/types/graph_update_params.py +++ b/src/writerai/types/graph_update_params.py @@ -2,14 +2,20 @@ from __future__ import annotations -from typing_extensions import Required, TypedDict +from typing_extensions import TypedDict __all__ = ["GraphUpdateParams"] class GraphUpdateParams(TypedDict, total=False): - name: Required[str] - """The name of the graph. This can be at most 255 characters.""" - description: str - """A description of the graph. This can be at most 255 characters.""" + """A description of the graph (max 255 characters). + + Omitting this field leaves the description unchanged. + """ + + name: str + """The name of the graph (max 255 characters). + + Omitting this field leaves the name unchanged. + """ diff --git a/src/writerai/types/question.py b/src/writerai/types/question.py index 4776dfbd..4d4d8871 100644 --- a/src/writerai/types/question.py +++ b/src/writerai/types/question.py @@ -15,7 +15,7 @@ class Subquery(BaseModel): query: str """The subquery that was asked.""" - sources: List[Source] + sources: List[Optional[Source]] class Question(BaseModel): @@ -25,6 +25,6 @@ class Question(BaseModel): question: str """The question that was asked.""" - sources: List[Source] + sources: List[Optional[Source]] - subqueries: Optional[List[Subquery]] = None + subqueries: Optional[List[Optional[Subquery]]] = None diff --git a/src/writerai/types/shared/graph_data.py b/src/writerai/types/shared/graph_data.py index b0f21bfb..ad78cf72 100644 --- a/src/writerai/types/shared/graph_data.py +++ b/src/writerai/types/shared/graph_data.py @@ -16,12 +16,12 @@ class Subquery(BaseModel): query: str """The subquery that was asked.""" - sources: List[Source] + sources: List[Optional[Source]] class GraphData(BaseModel): - sources: Optional[List[Source]] = None + sources: Optional[List[Optional[Source]]] = None status: Optional[Literal["processing", "finished"]] = None - subqueries: Optional[List[Subquery]] = None + subqueries: Optional[List[Optional[Subquery]]] = None diff --git a/src/writerai/types/shared_params/graph_data.py b/src/writerai/types/shared_params/graph_data.py index d89894c0..39ede602 100644 --- a/src/writerai/types/shared_params/graph_data.py +++ b/src/writerai/types/shared_params/graph_data.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Iterable +from typing import Iterable, Optional from typing_extensions import Literal, Required, TypedDict from .source import Source @@ -17,12 +17,12 @@ class Subquery(TypedDict, total=False): query: Required[str] """The subquery that was asked.""" - sources: Required[Iterable[Source]] + sources: Required[Iterable[Optional[Source]]] class GraphData(TypedDict, total=False): - sources: Iterable[Source] + sources: Iterable[Optional[Source]] - status: Literal["processing", "finished"] + status: Optional[Literal["processing", "finished"]] - subqueries: Iterable[Subquery] + subqueries: Iterable[Optional[Subquery]] diff --git a/tests/api_resources/test_graphs.py b/tests/api_resources/test_graphs.py index 1ce5771e..510af9e6 100644 --- a/tests/api_resources/test_graphs.py +++ b/tests/api_resources/test_graphs.py @@ -28,24 +28,20 @@ class TestGraphs: @parametrize def test_method_create(self, client: Writer) -> None: - graph = client.graphs.create( - name="name", - ) + graph = client.graphs.create() assert_matches_type(GraphCreateResponse, graph, path=["response"]) @parametrize def test_method_create_with_all_params(self, client: Writer) -> None: graph = client.graphs.create( - name="name", description="description", + name="name", ) assert_matches_type(GraphCreateResponse, graph, path=["response"]) @parametrize def test_raw_response_create(self, client: Writer) -> None: - response = client.graphs.with_raw_response.create( - name="name", - ) + response = client.graphs.with_raw_response.create() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -54,9 +50,7 @@ def test_raw_response_create(self, client: Writer) -> None: @parametrize def test_streaming_response_create(self, client: Writer) -> None: - with client.graphs.with_streaming_response.create( - name="name", - ) as response: + with client.graphs.with_streaming_response.create() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -107,7 +101,6 @@ def test_path_params_retrieve(self, client: Writer) -> None: def test_method_update(self, client: Writer) -> None: graph = client.graphs.update( graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - name="name", ) assert_matches_type(GraphUpdateResponse, graph, path=["response"]) @@ -115,8 +108,8 @@ def test_method_update(self, client: Writer) -> None: def test_method_update_with_all_params(self, client: Writer) -> None: graph = client.graphs.update( graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - name="name", description="description", + name="name", ) assert_matches_type(GraphUpdateResponse, graph, path=["response"]) @@ -124,7 +117,6 @@ def test_method_update_with_all_params(self, client: Writer) -> None: def test_raw_response_update(self, client: Writer) -> None: response = client.graphs.with_raw_response.update( graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - name="name", ) assert response.is_closed is True @@ -136,7 +128,6 @@ def test_raw_response_update(self, client: Writer) -> None: def test_streaming_response_update(self, client: Writer) -> None: with client.graphs.with_streaming_response.update( graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - name="name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -151,7 +142,6 @@ def test_path_params_update(self, client: Writer) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `graph_id` but received ''"): client.graphs.with_raw_response.update( graph_id="", - name="name", ) @parametrize @@ -402,24 +392,20 @@ class TestAsyncGraphs: @parametrize async def test_method_create(self, async_client: AsyncWriter) -> None: - graph = await async_client.graphs.create( - name="name", - ) + graph = await async_client.graphs.create() assert_matches_type(GraphCreateResponse, graph, path=["response"]) @parametrize async def test_method_create_with_all_params(self, async_client: AsyncWriter) -> None: graph = await async_client.graphs.create( - name="name", description="description", + name="name", ) assert_matches_type(GraphCreateResponse, graph, path=["response"]) @parametrize async def test_raw_response_create(self, async_client: AsyncWriter) -> None: - response = await async_client.graphs.with_raw_response.create( - name="name", - ) + response = await async_client.graphs.with_raw_response.create() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -428,9 +414,7 @@ async def test_raw_response_create(self, async_client: AsyncWriter) -> None: @parametrize async def test_streaming_response_create(self, async_client: AsyncWriter) -> None: - async with async_client.graphs.with_streaming_response.create( - name="name", - ) as response: + async with async_client.graphs.with_streaming_response.create() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -481,7 +465,6 @@ async def test_path_params_retrieve(self, async_client: AsyncWriter) -> None: async def test_method_update(self, async_client: AsyncWriter) -> None: graph = await async_client.graphs.update( graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - name="name", ) assert_matches_type(GraphUpdateResponse, graph, path=["response"]) @@ -489,8 +472,8 @@ async def test_method_update(self, async_client: AsyncWriter) -> None: async def test_method_update_with_all_params(self, async_client: AsyncWriter) -> None: graph = await async_client.graphs.update( graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - name="name", description="description", + name="name", ) assert_matches_type(GraphUpdateResponse, graph, path=["response"]) @@ -498,7 +481,6 @@ async def test_method_update_with_all_params(self, async_client: AsyncWriter) -> async def test_raw_response_update(self, async_client: AsyncWriter) -> None: response = await async_client.graphs.with_raw_response.update( graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - name="name", ) assert response.is_closed is True @@ -510,7 +492,6 @@ async def test_raw_response_update(self, async_client: AsyncWriter) -> None: async def test_streaming_response_update(self, async_client: AsyncWriter) -> None: async with async_client.graphs.with_streaming_response.update( graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - name="name", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -525,7 +506,6 @@ async def test_path_params_update(self, async_client: AsyncWriter) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `graph_id` but received ''"): await async_client.graphs.with_raw_response.update( graph_id="", - name="name", ) @parametrize From beb2f0348b8c306cc4c698def41609664a590613 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 18 Feb 2025 17:07:01 +0000 Subject: [PATCH 184/399] chore(internal): codegen related update (#186) --- README.md | 18 ++++++++++++++++++ src/writerai/_files.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 334a0e2d..ab908506 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,24 @@ for graph in first_page.data: # Remove `await` for non-async usage. ``` +## File uploads + +Request parameters that correspond to file uploads can be passed as `bytes`, a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance or a tuple of `(filename, contents, media type)`. + +```python +from pathlib import Path +from writerai import Writer + +client = Writer() + +client.files.upload( + content=Path("/path/to/file"), + content_disposition="Content-Disposition", +) +``` + +The async client uses the exact same interface. If you pass a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance, the file contents will be read asynchronously automatically. + ## Handling errors When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `writerai.APIConnectionError` is raised. diff --git a/src/writerai/_files.py b/src/writerai/_files.py index 715cc207..f6f78ade 100644 --- a/src/writerai/_files.py +++ b/src/writerai/_files.py @@ -34,7 +34,7 @@ def assert_is_file_content(obj: object, *, key: str | None = None) -> None: if not is_file_content(obj): prefix = f"Expected entry at `{key}`" if key is not None else f"Expected file input `{obj!r}`" raise RuntimeError( - f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead." + f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead. See https://github.com/writer/writer-python/tree/main#file-uploads" ) from None From a35ccdcf621f2a62c3961b79bcb6f7dc2ee840a0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 20 Feb 2025 22:34:15 +0000 Subject: [PATCH 185/399] feat(client): allow passing `NotGiven` for body (#187) fix(client): mark some request bodies as optional --- src/writerai/_base_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index c67010f7..cb751220 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -518,7 +518,7 @@ def _build_request( # so that passing a `TypedDict` doesn't cause an error. # https://github.com/microsoft/pyright/issues/3526#event-6715453066 params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None, - json=json_data, + json=json_data if is_given(json_data) else None, files=files, **kwargs, ) From 6b01683a54b80bc7a7a2ba17202120fd6d2be44b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 21 Feb 2025 14:59:59 +0000 Subject: [PATCH 186/399] chore(internal): fix devcontainers setup (#188) --- .devcontainer/Dockerfile | 2 +- .devcontainer/devcontainer.json | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index ac9a2e75..55d20255 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -6,4 +6,4 @@ USER vscode RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.35.0" RYE_INSTALL_OPTION="--yes" bash ENV PATH=/home/vscode/.rye/shims:$PATH -RUN echo "[[ -d .venv ]] && source .venv/bin/activate" >> /home/vscode/.bashrc +RUN echo "[[ -d .venv ]] && source .venv/bin/activate || export PATH=\$PATH" >> /home/vscode/.bashrc diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index bbeb30b1..c17fdc16 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -24,6 +24,9 @@ } } } + }, + "features": { + "ghcr.io/devcontainers/features/node:1": {} } // Features to add to the dev container. More info: https://containers.dev/features. From 49c2192e4df1b892e43ebf311755558deaa92d2f Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Fri, 21 Feb 2025 14:30:54 -0500 Subject: [PATCH 187/399] fix(internal): remove old src directories off of generated --- src/writer/lib/.keep | 4 ---- src/writer_ai/lib/.keep | 4 ---- 2 files changed, 8 deletions(-) delete mode 100644 src/writer/lib/.keep delete mode 100644 src/writer_ai/lib/.keep diff --git a/src/writer/lib/.keep b/src/writer/lib/.keep deleted file mode 100644 index 5e2c99fd..00000000 --- a/src/writer/lib/.keep +++ /dev/null @@ -1,4 +0,0 @@ -File generated from our OpenAPI spec by Stainless. - -This directory can be used to store custom files to expand the SDK. -It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. \ No newline at end of file diff --git a/src/writer_ai/lib/.keep b/src/writer_ai/lib/.keep deleted file mode 100644 index 5e2c99fd..00000000 --- a/src/writer_ai/lib/.keep +++ /dev/null @@ -1,4 +0,0 @@ -File generated from our OpenAPI spec by Stainless. - -This directory can be used to store custom files to expand the SDK. -It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. \ No newline at end of file From e86888f1b73f2c6bedec659d7b93f813dc2bc9ea Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 21 Feb 2025 19:42:28 +0000 Subject: [PATCH 188/399] docs: README code sample updates (#189) --- README.md | 75 ++++++++++++++++++++++++++++++++++---------- tests/test_client.py | 44 +++++++++++++++++++++++--- 2 files changed, 98 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index ab908506..08f15ca6 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,12 @@ client = Writer( ) chat_completion = client.chat.chat( - messages=[{"role": "user"}], + messages=[ + { + "content": "Write a poem about Python", + "role": "user", + } + ], model="palmyra-x-004", ) print(chat_completion.id) @@ -59,7 +64,12 @@ client = AsyncWriter( async def main() -> None: chat_completion = await client.chat.chat( - messages=[{"role": "user"}], + messages=[ + { + "content": "Write a poem about Python", + "role": "user", + } + ], model="palmyra-x-004", ) print(chat_completion.id) @@ -79,13 +89,18 @@ from writerai import Writer client = Writer() -stream = client.completions.create( - model="palmyra-x-003-instruct", - prompt="Hi, my name is", +stream = client.chat.chat( + messages=[ + { + "content": "Write a poem about Python", + "role": "user", + } + ], + model="palmyra-x-004", stream=True, ) -for completion in stream: - print(completion.choices) +for chat_completion in stream: + print(chat_completion.id) ``` The async client uses the exact same interface. @@ -95,13 +110,18 @@ from writerai import AsyncWriter client = AsyncWriter() -stream = await client.completions.create( - model="palmyra-x-003-instruct", - prompt="Hi, my name is", +stream = await client.chat.chat( + messages=[ + { + "content": "Write a poem about Python", + "role": "user", + } + ], + model="palmyra-x-004", stream=True, ) -async for completion in stream: - print(completion.choices) +async for chat_completion in stream: + print(chat_completion.id) ``` ## Using types @@ -211,7 +231,12 @@ client = Writer() try: client.chat.chat( - messages=[{"role": "user"}], + messages=[ + { + "content": "Write a poem about Python", + "role": "user", + } + ], model="palmyra-x-004", ) except writerai.APIConnectionError as e: @@ -257,7 +282,12 @@ client = Writer( # Or, configure per-request: client.with_options(max_retries=5).chat.chat( - messages=[{"role": "user"}], + messages=[ + { + "content": "Write a poem about Python", + "role": "user", + } + ], model="palmyra-x-004", ) ``` @@ -283,7 +313,12 @@ client = Writer( # Override per-request: client.with_options(timeout=5.0).chat.chat( - messages=[{"role": "user"}], + messages=[ + { + "content": "Write a poem about Python", + "role": "user", + } + ], model="palmyra-x-004", ) ``` @@ -328,7 +363,8 @@ from writerai import Writer client = Writer() response = client.chat.with_raw_response.chat( messages=[{ - "role": "user" + "content": "Write a poem about Python", + "role": "user", }], model="palmyra-x-004", ) @@ -350,7 +386,12 @@ To stream the response body, use `.with_streaming_response` instead, which requi ```python with client.chat.with_streaming_response.chat( - messages=[{"role": "user"}], + messages=[ + { + "content": "Write a poem about Python", + "role": "user", + } + ], model="palmyra-x-004", ) as response: print(response.headers.get("X-My-Header")) diff --git a/tests/test_client.py b/tests/test_client.py index 2dc34c7c..7d9acb8e 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -732,7 +732,16 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> No body=cast( object, maybe_transform( - dict(messages=[{"role": "user"}], model="palmyra-x-004"), ChatChatParamsNonStreaming + dict( + messages=[ + { + "content": "Write a poem about Python", + "role": "user", + } + ], + model="palmyra-x-004", + ), + ChatChatParamsNonStreaming, ), ), cast_to=httpx.Response, @@ -752,7 +761,16 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> Non body=cast( object, maybe_transform( - dict(messages=[{"role": "user"}], model="palmyra-x-004"), ChatChatParamsNonStreaming + dict( + messages=[ + { + "content": "Write a poem about Python", + "role": "user", + } + ], + model="palmyra-x-004", + ), + ChatChatParamsNonStreaming, ), ), cast_to=httpx.Response, @@ -1534,7 +1552,16 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) body=cast( object, maybe_transform( - dict(messages=[{"role": "user"}], model="palmyra-x-004"), ChatChatParamsNonStreaming + dict( + messages=[ + { + "content": "Write a poem about Python", + "role": "user", + } + ], + model="palmyra-x-004", + ), + ChatChatParamsNonStreaming, ), ), cast_to=httpx.Response, @@ -1554,7 +1581,16 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) body=cast( object, maybe_transform( - dict(messages=[{"role": "user"}], model="palmyra-x-004"), ChatChatParamsNonStreaming + dict( + messages=[ + { + "content": "Write a poem about Python", + "role": "user", + } + ], + model="palmyra-x-004", + ), + ChatChatParamsNonStreaming, ), ), cast_to=httpx.Response, From 0d8fd4dde356d2015a15efd7049dd93400c3876c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 21 Feb 2025 22:47:17 +0000 Subject: [PATCH 189/399] chore(internal): version bump (#194) --- .release-please-manifest.json | 2 +- README.md | 2 +- pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 59565e8e..65cf5877 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.6.1" + ".": "2.0.0-rc1" } \ No newline at end of file diff --git a/README.md b/README.md index 08f15ca6..c32e9ce4 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The REST API documentation can be found on [dev.writer.com](https://dev.writer.c ```sh # install from PyPI -pip install writer-sdk +pip install --pre writer-sdk ``` ## Usage diff --git a/pyproject.toml b/pyproject.toml index 91d50569..e9bd6290 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "1.6.1" +version = "2.0.0-rc1" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index 3f139641..82471686 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "1.6.1" # x-release-please-version +__version__ = "2.0.0-rc1" # x-release-please-version From d733926c3d8ba0f5cafe62623298fc1028427eba Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 25 Feb 2025 00:01:42 +0000 Subject: [PATCH 190/399] docs(api): updates to API spec (#196) --- .stats.yml | 2 +- src/writerai/resources/applications/graphs.py | 24 +++---- src/writerai/resources/graphs.py | 66 +++++++++---------- .../application_graphs_response.py | 2 +- .../types/applications/graph_update_params.py | 6 +- src/writerai/types/file.py | 2 +- src/writerai/types/graph.py | 14 ++-- src/writerai/types/graph_create_params.py | 4 +- src/writerai/types/graph_create_response.py | 8 +-- src/writerai/types/graph_delete_response.py | 4 +- src/writerai/types/graph_update_params.py | 4 +- src/writerai/types/graph_update_response.py | 8 +-- 12 files changed, 71 insertions(+), 73 deletions(-) diff --git a/.stats.yml b/.stats.yml index a6d1f570..cbb837fb 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 29 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-9f17b2629bd54f56bc3e48ec710b11e2ba84302725f8da9e9ed390bbed5d3b5b.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-558b786e136e5c51da3c50609594c503853068e64efd30aae8e6139bb0cee8f0.yml diff --git a/src/writerai/resources/applications/graphs.py b/src/writerai/resources/applications/graphs.py index f393f660..5e9a7fc6 100644 --- a/src/writerai/resources/applications/graphs.py +++ b/src/writerai/resources/applications/graphs.py @@ -59,13 +59,13 @@ def update( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ApplicationGraphsResponse: """ - Updates the graphs listed and associates them with the no-code chat app to be - used. + Updates the Knowledge Graphs listed and associates them with the no-code chat + app to be used. Args: - graph_ids: A list of graph IDs to associate with the application. Note that this will - replace the existing list of graphs associated with the application, not add to - it. + graph_ids: A list of Knowledge Graph IDs to associate with the application. Note that this + will replace the existing list of Knowledge Graphs associated with the + application, not add to it. extra_headers: Send extra headers @@ -98,7 +98,7 @@ def list( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ApplicationGraphsResponse: """ - Retrieve graphs associated with a no-code chat application. + Retrieve Knowledge Graphs associated with a no-code chat application. Args: extra_headers: Send extra headers @@ -153,13 +153,13 @@ async def update( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ApplicationGraphsResponse: """ - Updates the graphs listed and associates them with the no-code chat app to be - used. + Updates the Knowledge Graphs listed and associates them with the no-code chat + app to be used. Args: - graph_ids: A list of graph IDs to associate with the application. Note that this will - replace the existing list of graphs associated with the application, not add to - it. + graph_ids: A list of Knowledge Graph IDs to associate with the application. Note that this + will replace the existing list of Knowledge Graphs associated with the + application, not add to it. extra_headers: Send extra headers @@ -192,7 +192,7 @@ async def list( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ApplicationGraphsResponse: """ - Retrieve graphs associated with a no-code chat application. + Retrieve Knowledge Graphs associated with a no-code chat application. Args: extra_headers: Send extra headers diff --git a/src/writerai/resources/graphs.py b/src/writerai/resources/graphs.py index 7ea696a7..1833f762 100644 --- a/src/writerai/resources/graphs.py +++ b/src/writerai/resources/graphs.py @@ -76,14 +76,14 @@ def create( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> GraphCreateResponse: """ - Create graph + Create a new Knowledge Graph. Args: - description: A description of the graph (max 255 characters). Omitting this field leaves the - description unchanged. + description: A description of the Knowledge Graph (max 255 characters). Omitting this field + leaves the description unchanged. - name: The name of the graph (max 255 characters). Omitting this field leaves the name - unchanged. + name: The name of the Knowledge Graph (max 255 characters). Omitting this field leaves + the name unchanged. extra_headers: Send extra headers @@ -120,7 +120,7 @@ def retrieve( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> Graph: """ - Retrieve graph + Retrieve a Knowledge Graph. Args: extra_headers: Send extra headers @@ -158,11 +158,11 @@ def update( Update the name and description of a Knowledge Graph. Args: - description: A description of the graph (max 255 characters). Omitting this field leaves the - description unchanged. + description: A description of the Knowledge Graph (max 255 characters). Omitting this field + leaves the description unchanged. - name: The name of the graph (max 255 characters). Omitting this field leaves the name - unchanged. + name: The name of the Knowledge Graph (max 255 characters). Omitting this field leaves + the name unchanged. extra_headers: Send extra headers @@ -203,12 +203,11 @@ def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> SyncCursorPage[Graph]: - """List graphs + """ + Retrieve a list of Knowledge Graphs. Args: - after: The ID of the last object in the previous page. - - This parameter instructs the API + after: The ID of the last object in the previous page. This parameter instructs the API to return the next page of results. before: The ID of the first object in the previous page. This parameter instructs the @@ -261,7 +260,7 @@ def delete( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> GraphDeleteResponse: """ - Delete graph + Delete a Knowledge Graph. Args: extra_headers: Send extra headers @@ -295,7 +294,7 @@ def add_file_to_graph( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> File: """ - Add file to graph + Add a file to a Knowledge Graph. Args: file_id: The unique identifier of the file. @@ -483,7 +482,7 @@ def remove_file_from_graph( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> GraphRemoveFileFromGraphResponse: """ - Remove file from graph + Remove a file from a Knowledge Graph. Args: extra_headers: Send extra headers @@ -540,14 +539,14 @@ async def create( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> GraphCreateResponse: """ - Create graph + Create a new Knowledge Graph. Args: - description: A description of the graph (max 255 characters). Omitting this field leaves the - description unchanged. + description: A description of the Knowledge Graph (max 255 characters). Omitting this field + leaves the description unchanged. - name: The name of the graph (max 255 characters). Omitting this field leaves the name - unchanged. + name: The name of the Knowledge Graph (max 255 characters). Omitting this field leaves + the name unchanged. extra_headers: Send extra headers @@ -584,7 +583,7 @@ async def retrieve( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> Graph: """ - Retrieve graph + Retrieve a Knowledge Graph. Args: extra_headers: Send extra headers @@ -622,11 +621,11 @@ async def update( Update the name and description of a Knowledge Graph. Args: - description: A description of the graph (max 255 characters). Omitting this field leaves the - description unchanged. + description: A description of the Knowledge Graph (max 255 characters). Omitting this field + leaves the description unchanged. - name: The name of the graph (max 255 characters). Omitting this field leaves the name - unchanged. + name: The name of the Knowledge Graph (max 255 characters). Omitting this field leaves + the name unchanged. extra_headers: Send extra headers @@ -667,12 +666,11 @@ def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> AsyncPaginator[Graph, AsyncCursorPage[Graph]]: - """List graphs + """ + Retrieve a list of Knowledge Graphs. Args: - after: The ID of the last object in the previous page. - - This parameter instructs the API + after: The ID of the last object in the previous page. This parameter instructs the API to return the next page of results. before: The ID of the first object in the previous page. This parameter instructs the @@ -725,7 +723,7 @@ async def delete( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> GraphDeleteResponse: """ - Delete graph + Delete a Knowledge Graph. Args: extra_headers: Send extra headers @@ -759,7 +757,7 @@ async def add_file_to_graph( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> File: """ - Add file to graph + Add a file to a Knowledge Graph. Args: file_id: The unique identifier of the file. @@ -949,7 +947,7 @@ async def remove_file_from_graph( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> GraphRemoveFileFromGraphResponse: """ - Remove file from graph + Remove a file from a Knowledge Graph. Args: extra_headers: Send extra headers diff --git a/src/writerai/types/applications/application_graphs_response.py b/src/writerai/types/applications/application_graphs_response.py index 6b87ceb9..f979a58a 100644 --- a/src/writerai/types/applications/application_graphs_response.py +++ b/src/writerai/types/applications/application_graphs_response.py @@ -9,4 +9,4 @@ class ApplicationGraphsResponse(BaseModel): graph_ids: List[str] - """A list of graphs associated with the application.""" + """A list of Knowledge Graphs associated with the application.""" diff --git a/src/writerai/types/applications/graph_update_params.py b/src/writerai/types/applications/graph_update_params.py index ea7be169..8a3bd87c 100644 --- a/src/writerai/types/applications/graph_update_params.py +++ b/src/writerai/types/applications/graph_update_params.py @@ -10,8 +10,8 @@ class GraphUpdateParams(TypedDict, total=False): graph_ids: Required[List[str]] - """A list of graph IDs to associate with the application. + """A list of Knowledge Graph IDs to associate with the application. - Note that this will replace the existing list of graphs associated with the - application, not add to it. + Note that this will replace the existing list of Knowledge Graphs associated + with the application, not add to it. """ diff --git a/src/writerai/types/file.py b/src/writerai/types/file.py index 7d708f50..f12ba530 100644 --- a/src/writerai/types/file.py +++ b/src/writerai/types/file.py @@ -16,7 +16,7 @@ class File(BaseModel): """The timestamp when the file was uploaded.""" graph_ids: List[str] - """A list of graph IDs that the file is associated with.""" + """A list of Knowledge Graph IDs that the file is associated with.""" name: str """The name of the file.""" diff --git a/src/writerai/types/graph.py b/src/writerai/types/graph.py index 51bd9cc1..02dcf94e 100644 --- a/src/writerai/types/graph.py +++ b/src/writerai/types/graph.py @@ -20,26 +20,26 @@ class FileStatus(BaseModel): """The number of files currently being processed.""" total: int - """The total number of files associated with the graph.""" + """The total number of files associated with the Knowledge Graph.""" class Graph(BaseModel): id: str - """A unique identifier of the graph.""" + """The unique identifier of the Knowledge Graph.""" created_at: datetime - """The timestamp when the graph was created.""" + """The timestamp when the Knowledge Graph was created.""" file_status: FileStatus name: str - """The name of the graph.""" + """The name of the Knowledge Graph.""" type: Literal["manual", "connector"] """ - The type of graph, either `manual` (files are uploaded via UI or API) or - `connector` (files are uploaded via a connector). + The type of Knowledge Graph, either `manual` (files are uploaded via UI or API) + or `connector` (files are uploaded via a connector). """ description: Optional[str] = None - """A description of the graph.""" + """A description of the Knowledge Graph.""" diff --git a/src/writerai/types/graph_create_params.py b/src/writerai/types/graph_create_params.py index 939e02a2..30f2b213 100644 --- a/src/writerai/types/graph_create_params.py +++ b/src/writerai/types/graph_create_params.py @@ -9,13 +9,13 @@ class GraphCreateParams(TypedDict, total=False): description: str - """A description of the graph (max 255 characters). + """A description of the Knowledge Graph (max 255 characters). Omitting this field leaves the description unchanged. """ name: str - """The name of the graph (max 255 characters). + """The name of the Knowledge Graph (max 255 characters). Omitting this field leaves the name unchanged. """ diff --git a/src/writerai/types/graph_create_response.py b/src/writerai/types/graph_create_response.py index 87890b88..7564f4f8 100644 --- a/src/writerai/types/graph_create_response.py +++ b/src/writerai/types/graph_create_response.py @@ -10,13 +10,13 @@ class GraphCreateResponse(BaseModel): id: str - """A unique identifier of the graph.""" + """A unique identifier of the Knowledge Graph.""" created_at: datetime - """The timestamp when the graph was created.""" + """The timestamp when the Knowledge Graph was created.""" name: str - """The name of the graph. This can be at most 255 characters.""" + """The name of the Knowledge Graph (max 255 characters).""" description: Optional[str] = None - """A description of the graph. This can be at most 255 characters.""" + """A description of the Knowledge Graph (max 255 characters).""" diff --git a/src/writerai/types/graph_delete_response.py b/src/writerai/types/graph_delete_response.py index a91b734c..40b62701 100644 --- a/src/writerai/types/graph_delete_response.py +++ b/src/writerai/types/graph_delete_response.py @@ -8,7 +8,7 @@ class GraphDeleteResponse(BaseModel): id: str - """A unique identifier of the deleted graph.""" + """A unique identifier of the deleted Knowledge Graph.""" deleted: bool - """Indicates whether the graph was successfully deleted.""" + """Indicates whether the Knowledge Graph was successfully deleted.""" diff --git a/src/writerai/types/graph_update_params.py b/src/writerai/types/graph_update_params.py index 61f1bb52..03d36d52 100644 --- a/src/writerai/types/graph_update_params.py +++ b/src/writerai/types/graph_update_params.py @@ -9,13 +9,13 @@ class GraphUpdateParams(TypedDict, total=False): description: str - """A description of the graph (max 255 characters). + """A description of the Knowledge Graph (max 255 characters). Omitting this field leaves the description unchanged. """ name: str - """The name of the graph (max 255 characters). + """The name of the Knowledge Graph (max 255 characters). Omitting this field leaves the name unchanged. """ diff --git a/src/writerai/types/graph_update_response.py b/src/writerai/types/graph_update_response.py index de4ccbcd..3e43b2e5 100644 --- a/src/writerai/types/graph_update_response.py +++ b/src/writerai/types/graph_update_response.py @@ -10,13 +10,13 @@ class GraphUpdateResponse(BaseModel): id: str - """A unique identifier of the graph.""" + """A unique identifier of the Knowledge Graph.""" created_at: datetime - """The timestamp when the graph was created.""" + """The timestamp when the Knowledge Graph was created.""" name: str - """The name of the graph. This can be at most 255 characters.""" + """The name of the Knowledge Graph (max 255 characters).""" description: Optional[str] = None - """A description of the graph. This can be at most 255 characters.""" + """A description of the Knowledge Graph (max 255 characters).""" From 1d7b963527afaf1c24a249becea0f402dcfa7294 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 25 Feb 2025 11:00:43 +0000 Subject: [PATCH 191/399] chore(internal): properly set __pydantic_private__ (#197) --- src/writerai/_base_client.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index cb751220..9b12ed3c 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -63,7 +63,7 @@ ModelBuilderProtocol, ) from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping -from ._compat import model_copy, model_dump +from ._compat import PYDANTIC_V2, model_copy, model_dump from ._models import GenericModel, FinalRequestOptions, validate_type, construct_type from ._response import ( APIResponse, @@ -207,6 +207,9 @@ def _set_private_attributes( model: Type[_T], options: FinalRequestOptions, ) -> None: + if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None: + self.__pydantic_private__ = {} + self._model = model self._client = client self._options = options @@ -292,6 +295,9 @@ def _set_private_attributes( client: AsyncAPIClient, options: FinalRequestOptions, ) -> None: + if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None: + self.__pydantic_private__ = {} + self._model = model self._client = client self._options = options From 421fbd561ac825fcaba6225bef26ad172b47db88 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 25 Feb 2025 19:07:31 +0000 Subject: [PATCH 192/399] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index cbb837fb..9ae4d8b3 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 29 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-558b786e136e5c51da3c50609594c503853068e64efd30aae8e6139bb0cee8f0.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-3d3b2fe43375eac35441f6de554253a7dc3eaa5d97c00a5a351ec72d6587eb32.yml From 261ce17ba3e280ebd2e7dc1788ec52a2e3632e59 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 26 Feb 2025 18:04:37 +0000 Subject: [PATCH 193/399] chore(internal): version bump (#200) --- .release-please-manifest.json | 2 +- README.md | 2 +- pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 65cf5877..65f558e7 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.0.0-rc1" + ".": "2.0.0" } \ No newline at end of file diff --git a/README.md b/README.md index c32e9ce4..08f15ca6 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The REST API documentation can be found on [dev.writer.com](https://dev.writer.c ```sh # install from PyPI -pip install --pre writer-sdk +pip install writer-sdk ``` ## Usage diff --git a/pyproject.toml b/pyproject.toml index e9bd6290..3cc82969 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "2.0.0-rc1" +version = "2.0.0" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index 82471686..a7140c47 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "2.0.0-rc1" # x-release-please-version +__version__ = "2.0.0" # x-release-please-version From b5bf6814e3bf0a7fe7cfc5711bf806033ee38460 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 27 Feb 2025 21:56:14 +0000 Subject: [PATCH 194/399] docs: update URLs from stainlessapi.com to stainless.com (#202) More details at https://www.stainless.com/changelog/stainless-com --- README.md | 2 +- SECURITY.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 08f15ca6..02930aa1 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ The Writer Python library provides convenient access to the Writer REST API from 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.stainlessapi.com/). +It is generated with [Stainless](https://www.stainless.com/). ## Documentation diff --git a/SECURITY.md b/SECURITY.md index f08c9053..b7b1acaa 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,9 +2,9 @@ ## Reporting Security Issues -This SDK is generated by [Stainless Software Inc](http://stainlessapi.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken. +This SDK is generated by [Stainless Software Inc](http://stainless.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken. -To report a security issue, please contact the Stainless team at security@stainlessapi.com. +To report a security issue, please contact the Stainless team at security@stainless.com. ## Responsible Disclosure From e3adcb478e3bb9c264025196aecdcb1f80752788 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 27 Feb 2025 22:47:56 +0000 Subject: [PATCH 195/399] chore(docs): update client docstring (#203) --- src/writerai/_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/writerai/_client.py b/src/writerai/_client.py index 0a6f5a0f..d5b8e949 100644 --- a/src/writerai/_client.py +++ b/src/writerai/_client.py @@ -75,7 +75,7 @@ def __init__( # part of our public interface in the future. _strict_response_validation: bool = False, ) -> None: - """Construct a new synchronous writer client instance. + """Construct a new synchronous Writer client instance. This automatically infers the `api_key` argument from the `WRITER_API_KEY` environment variable if it is not provided. """ @@ -257,7 +257,7 @@ def __init__( # part of our public interface in the future. _strict_response_validation: bool = False, ) -> None: - """Construct a new async writer client instance. + """Construct a new async AsyncWriter client instance. This automatically infers the `api_key` argument from the `WRITER_API_KEY` environment variable if it is not provided. """ From 77da2369492763f05c7c261e3e98c6f518dcd5f7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 20:35:12 +0000 Subject: [PATCH 196/399] chore(internal): codegen related update (#204) --- src/writerai/_base_client.py | 97 +----------------------------------- 1 file changed, 1 insertion(+), 96 deletions(-) diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index 9b12ed3c..36bb1271 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -9,7 +9,6 @@ import inspect import logging import platform -import warnings import email.utils from types import TracebackType from random import random @@ -36,7 +35,7 @@ import httpx import distro import pydantic -from httpx import URL, Limits +from httpx import URL from pydantic import PrivateAttr from . import _exceptions @@ -51,13 +50,10 @@ Timeout, NotGiven, ResponseT, - Transport, AnyMapping, PostParser, - ProxiesTypes, RequestFiles, HttpxSendArgs, - AsyncTransport, RequestOptions, HttpxRequestFiles, ModelBuilderProtocol, @@ -337,9 +333,6 @@ class BaseClient(Generic[_HttpxClientT, _DefaultStreamT]): _base_url: URL max_retries: int timeout: Union[float, Timeout, None] - _limits: httpx.Limits - _proxies: ProxiesTypes | None - _transport: Transport | AsyncTransport | None _strict_response_validation: bool _idempotency_header: str | None _default_stream_cls: type[_DefaultStreamT] | None = None @@ -352,9 +345,6 @@ def __init__( _strict_response_validation: bool, max_retries: int = DEFAULT_MAX_RETRIES, timeout: float | Timeout | None = DEFAULT_TIMEOUT, - limits: httpx.Limits, - transport: Transport | AsyncTransport | None, - proxies: ProxiesTypes | None, custom_headers: Mapping[str, str] | None = None, custom_query: Mapping[str, object] | None = None, ) -> None: @@ -362,9 +352,6 @@ def __init__( self._base_url = self._enforce_trailing_slash(URL(base_url)) self.max_retries = max_retries self.timeout = timeout - self._limits = limits - self._proxies = proxies - self._transport = transport self._custom_headers = custom_headers or {} self._custom_query = custom_query or {} self._strict_response_validation = _strict_response_validation @@ -800,46 +787,11 @@ def __init__( base_url: str | URL, max_retries: int = DEFAULT_MAX_RETRIES, timeout: float | Timeout | None | NotGiven = NOT_GIVEN, - transport: Transport | None = None, - proxies: ProxiesTypes | None = None, - limits: Limits | None = None, http_client: httpx.Client | None = None, custom_headers: Mapping[str, str] | None = None, custom_query: Mapping[str, object] | None = None, _strict_response_validation: bool, ) -> None: - kwargs: dict[str, Any] = {} - if limits is not None: - warnings.warn( - "The `connection_pool_limits` argument is deprecated. The `http_client` argument should be passed instead", - category=DeprecationWarning, - stacklevel=3, - ) - if http_client is not None: - raise ValueError("The `http_client` argument is mutually exclusive with `connection_pool_limits`") - else: - limits = DEFAULT_CONNECTION_LIMITS - - if transport is not None: - kwargs["transport"] = transport - warnings.warn( - "The `transport` argument is deprecated. The `http_client` argument should be passed instead", - category=DeprecationWarning, - stacklevel=3, - ) - if http_client is not None: - raise ValueError("The `http_client` argument is mutually exclusive with `transport`") - - if proxies is not None: - kwargs["proxies"] = proxies - warnings.warn( - "The `proxies` argument is deprecated. The `http_client` argument should be passed instead", - category=DeprecationWarning, - stacklevel=3, - ) - if http_client is not None: - raise ValueError("The `http_client` argument is mutually exclusive with `proxies`") - if not is_given(timeout): # if the user passed in a custom http client with a non-default # timeout set then we use that timeout. @@ -860,12 +812,9 @@ def __init__( super().__init__( version=version, - limits=limits, # cast to a valid type because mypy doesn't understand our type narrowing timeout=cast(Timeout, timeout), - proxies=proxies, base_url=base_url, - transport=transport, max_retries=max_retries, custom_query=custom_query, custom_headers=custom_headers, @@ -875,9 +824,6 @@ def __init__( base_url=base_url, # cast to a valid type because mypy doesn't understand our type narrowing timeout=cast(Timeout, timeout), - limits=limits, - follow_redirects=True, - **kwargs, # type: ignore ) def is_closed(self) -> bool: @@ -1372,45 +1318,10 @@ def __init__( _strict_response_validation: bool, max_retries: int = DEFAULT_MAX_RETRIES, timeout: float | Timeout | None | NotGiven = NOT_GIVEN, - transport: AsyncTransport | None = None, - proxies: ProxiesTypes | None = None, - limits: Limits | None = None, http_client: httpx.AsyncClient | None = None, custom_headers: Mapping[str, str] | None = None, custom_query: Mapping[str, object] | None = None, ) -> None: - kwargs: dict[str, Any] = {} - if limits is not None: - warnings.warn( - "The `connection_pool_limits` argument is deprecated. The `http_client` argument should be passed instead", - category=DeprecationWarning, - stacklevel=3, - ) - if http_client is not None: - raise ValueError("The `http_client` argument is mutually exclusive with `connection_pool_limits`") - else: - limits = DEFAULT_CONNECTION_LIMITS - - if transport is not None: - kwargs["transport"] = transport - warnings.warn( - "The `transport` argument is deprecated. The `http_client` argument should be passed instead", - category=DeprecationWarning, - stacklevel=3, - ) - if http_client is not None: - raise ValueError("The `http_client` argument is mutually exclusive with `transport`") - - if proxies is not None: - kwargs["proxies"] = proxies - warnings.warn( - "The `proxies` argument is deprecated. The `http_client` argument should be passed instead", - category=DeprecationWarning, - stacklevel=3, - ) - if http_client is not None: - raise ValueError("The `http_client` argument is mutually exclusive with `proxies`") - if not is_given(timeout): # if the user passed in a custom http client with a non-default # timeout set then we use that timeout. @@ -1432,11 +1343,8 @@ def __init__( super().__init__( version=version, base_url=base_url, - limits=limits, # cast to a valid type because mypy doesn't understand our type narrowing timeout=cast(Timeout, timeout), - proxies=proxies, - transport=transport, max_retries=max_retries, custom_query=custom_query, custom_headers=custom_headers, @@ -1446,9 +1354,6 @@ def __init__( base_url=base_url, # cast to a valid type because mypy doesn't understand our type narrowing timeout=cast(Timeout, timeout), - limits=limits, - follow_redirects=True, - **kwargs, # type: ignore ) def is_closed(self) -> bool: From c2afbcb843b5f026b6dfdc1a10f2396b925857f0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 9 Mar 2025 21:38:38 +0000 Subject: [PATCH 197/399] docs: revise readme docs about nested params (#206) --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 02930aa1..6e9c8d07 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,23 @@ for graph in first_page.data: # Remove `await` for non-async usage. ``` +## Nested params + +Nested parameters are dictionaries, typed using `TypedDict`, for example: + +```python +from writerai import Writer + +client = Writer() + +chat_completion = client.chat.chat( + messages=[{"role": "user"}], + model="model", + stream_options={"include_usage": True}, +) +print(chat_completion.stream_options) +``` + ## File uploads Request parameters that correspond to file uploads can be passed as `bytes`, a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance or a tuple of `(filename, contents, media type)`. From 16c17ebc61b2105a775c15c46b04a3d50a16dcce Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 11 Mar 2025 01:06:26 +0000 Subject: [PATCH 198/399] test: add DEFER_PYDANTIC_BUILD=false flag to tests (#207) --- scripts/test | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/test b/scripts/test index 4fa5698b..2b878456 100755 --- a/scripts/test +++ b/scripts/test @@ -52,6 +52,8 @@ else echo fi +export DEFER_PYDANTIC_BUILD=false + echo "==> Running tests" rye run pytest "$@" From 791e4994dccca8f98cd7351d384ec6a436a49e6a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 11 Mar 2025 15:50:38 +0000 Subject: [PATCH 199/399] docs(api): updates to API spec (#208) --- .stats.yml | 2 +- src/writerai/types/shared/tool_call.py | 3 ++- src/writerai/types/shared/tool_call_streaming.py | 3 ++- src/writerai/types/shared/tool_param.py | 2 +- src/writerai/types/shared_params/tool_call.py | 4 ++-- src/writerai/types/shared_params/tool_param.py | 2 +- tests/api_resources/test_chat.py | 8 ++++---- 7 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.stats.yml b/.stats.yml index 9ae4d8b3..5c701db8 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 29 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-3d3b2fe43375eac35441f6de554253a7dc3eaa5d97c00a5a351ec72d6587eb32.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-350e8ec39a0511510d9af6d1ba3e60154d0585a8a8732e50a2fd40ede8881dc1.yml diff --git a/src/writerai/types/shared/tool_call.py b/src/writerai/types/shared/tool_call.py index d21d0cc1..33d5d929 100644 --- a/src/writerai/types/shared/tool_call.py +++ b/src/writerai/types/shared/tool_call.py @@ -1,6 +1,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import Optional +from typing_extensions import Literal from ..._models import BaseModel @@ -18,6 +19,6 @@ class ToolCall(BaseModel): function: Function - type: str + type: Literal["function"] index: Optional[int] = None diff --git a/src/writerai/types/shared/tool_call_streaming.py b/src/writerai/types/shared/tool_call_streaming.py index 350001f2..ad4619e6 100644 --- a/src/writerai/types/shared/tool_call_streaming.py +++ b/src/writerai/types/shared/tool_call_streaming.py @@ -1,6 +1,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import Optional +from typing_extensions import Literal from ..._models import BaseModel @@ -20,4 +21,4 @@ class ToolCallStreaming(BaseModel): function: Optional[Function] = None - type: Optional[str] = None + type: Optional[Literal["function"]] = None diff --git a/src/writerai/types/shared/tool_param.py b/src/writerai/types/shared/tool_param.py index c8761bab..addecc6e 100644 --- a/src/writerai/types/shared/tool_param.py +++ b/src/writerai/types/shared/tool_param.py @@ -49,7 +49,7 @@ class LlmTool(BaseModel): function: LlmToolFunction """A tool that uses another Writer model to generate a response.""" - type: Optional[Literal["llm"]] = None + type: Literal["llm"] """The type of tool.""" diff --git a/src/writerai/types/shared_params/tool_call.py b/src/writerai/types/shared_params/tool_call.py index 5a81f596..cb736487 100644 --- a/src/writerai/types/shared_params/tool_call.py +++ b/src/writerai/types/shared_params/tool_call.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing_extensions import Required, TypedDict +from typing_extensions import Literal, Required, TypedDict __all__ = ["ToolCall", "Function"] @@ -18,6 +18,6 @@ class ToolCall(TypedDict, total=False): function: Required[Function] - type: Required[str] + type: Required[Literal["function"]] index: int diff --git a/src/writerai/types/shared_params/tool_param.py b/src/writerai/types/shared_params/tool_param.py index 827e0820..fcb3f373 100644 --- a/src/writerai/types/shared_params/tool_param.py +++ b/src/writerai/types/shared_params/tool_param.py @@ -49,7 +49,7 @@ class LlmTool(TypedDict, total=False): function: Required[LlmToolFunction] """A tool that uses another Writer model to generate a response.""" - type: Literal["llm"] + type: Required[Literal["llm"]] """The type of tool.""" diff --git a/tests/api_resources/test_chat.py b/tests/api_resources/test_chat.py index a1c804be..c035240e 100644 --- a/tests/api_resources/test_chat.py +++ b/tests/api_resources/test_chat.py @@ -63,7 +63,7 @@ def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: "arguments": "arguments", "name": "name", }, - "type": "type", + "type": "function", "index": 0, } ], @@ -165,7 +165,7 @@ def test_method_chat_with_all_params_overload_2(self, client: Writer) -> None: "arguments": "arguments", "name": "name", }, - "type": "type", + "type": "function", "index": 0, } ], @@ -271,7 +271,7 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW "arguments": "arguments", "name": "name", }, - "type": "type", + "type": "function", "index": 0, } ], @@ -373,7 +373,7 @@ async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncW "arguments": "arguments", "name": "name", }, - "type": "type", + "type": "function", "index": 0, } ], From 744e7a7b3db98c0c2ada04e2da30047c4a825d18 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 13 Mar 2025 16:45:42 +0000 Subject: [PATCH 200/399] chore(internal): remove extra empty newlines (#210) --- pyproject.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3cc82969..19438628 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,6 @@ Homepage = "https://github.com/writer/writer-python" Repository = "https://github.com/writer/writer-python" - [tool.rye] managed = true # version pins are in requirements-dev.lock @@ -152,7 +151,6 @@ reportImplicitOverride = true reportImportCycles = false reportPrivateUsage = false - [tool.ruff] line-length = 120 output-format = "grouped" From fd3848f1b01624e85ebe0e8983ebfa630db95de1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 14 Mar 2025 15:17:29 +0000 Subject: [PATCH 201/399] chore(internal): bump rye to 0.44.0 (#211) --- .devcontainer/Dockerfile | 2 +- .github/workflows/ci.yml | 4 ++-- .github/workflows/publish-pypi.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 55d20255..ff261bad 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -3,7 +3,7 @@ FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} USER vscode -RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.35.0" RYE_INSTALL_OPTION="--yes" bash +RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.44.0" RYE_INSTALL_OPTION="--yes" bash ENV PATH=/home/vscode/.rye/shims:$PATH RUN echo "[[ -d .venv ]] && source .venv/bin/activate || export PATH=\$PATH" >> /home/vscode/.bashrc diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8a8a4f7..3b286e5a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: curl -sSf https://rye.astral.sh/get | bash echo "$HOME/.rye/shims" >> $GITHUB_PATH env: - RYE_VERSION: '0.35.0' + RYE_VERSION: '0.44.0' RYE_INSTALL_OPTION: '--yes' - name: Install dependencies @@ -42,7 +42,7 @@ jobs: curl -sSf https://rye.astral.sh/get | bash echo "$HOME/.rye/shims" >> $GITHUB_PATH env: - RYE_VERSION: '0.35.0' + RYE_VERSION: '0.44.0' RYE_INSTALL_OPTION: '--yes' - name: Bootstrap diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index e206653d..85424099 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -21,7 +21,7 @@ jobs: curl -sSf https://rye.astral.sh/get | bash echo "$HOME/.rye/shims" >> $GITHUB_PATH env: - RYE_VERSION: '0.35.0' + RYE_VERSION: '0.44.0' RYE_INSTALL_OPTION: '--yes' - name: Publish to PyPI From 919ee0e1e29f85147514e40bca89d97625ca7dac Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 14 Mar 2025 18:00:41 +0000 Subject: [PATCH 202/399] chore(internal): codegen related update (#212) --- requirements-dev.lock | 1 + requirements.lock | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements-dev.lock b/requirements-dev.lock index 21f1395b..857dd051 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -7,6 +7,7 @@ # all-features: true # with-sources: false # generate-hashes: false +# universal: false -e file:. annotated-types==0.6.0 diff --git a/requirements.lock b/requirements.lock index aae2f01e..8bd96abb 100644 --- a/requirements.lock +++ b/requirements.lock @@ -7,6 +7,7 @@ # all-features: true # with-sources: false # generate-hashes: false +# universal: false -e file:. annotated-types==0.6.0 From 5a3652b690ee5d9940756458a55286368e45e2af Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 14 Mar 2025 22:10:30 +0000 Subject: [PATCH 203/399] fix(types): handle more discriminated union shapes (#213) --- src/writerai/_models.py | 7 +++++-- tests/test_models.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/writerai/_models.py b/src/writerai/_models.py index c4401ff8..b51a1bf5 100644 --- a/src/writerai/_models.py +++ b/src/writerai/_models.py @@ -65,7 +65,7 @@ from ._constants import RAW_RESPONSE_HEADER if TYPE_CHECKING: - from pydantic_core.core_schema import ModelField, LiteralSchema, ModelFieldsSchema + from pydantic_core.core_schema import ModelField, ModelSchema, LiteralSchema, ModelFieldsSchema __all__ = ["BaseModel", "GenericModel"] @@ -646,15 +646,18 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, def _extract_field_schema_pv2(model: type[BaseModel], field_name: str) -> ModelField | None: schema = model.__pydantic_core_schema__ + if schema["type"] == "definitions": + schema = schema["schema"] + if schema["type"] != "model": return None + schema = cast("ModelSchema", schema) fields_schema = schema["schema"] if fields_schema["type"] != "model-fields": return None fields_schema = cast("ModelFieldsSchema", fields_schema) - field = fields_schema["fields"].get(field_name) if not field: return None diff --git a/tests/test_models.py b/tests/test_models.py index d0a41b28..91faed5a 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -854,3 +854,35 @@ class Model(BaseModel): m = construct_type(value={"cls": "foo"}, type_=Model) assert isinstance(m, Model) assert isinstance(m.cls, str) + + +def test_discriminated_union_case() -> None: + class A(BaseModel): + type: Literal["a"] + + data: bool + + class B(BaseModel): + type: Literal["b"] + + data: List[Union[A, object]] + + class ModelA(BaseModel): + type: Literal["modelA"] + + data: int + + class ModelB(BaseModel): + type: Literal["modelB"] + + required: str + + data: Union[A, B] + + # when constructing ModelA | ModelB, value data doesn't match ModelB exactly - missing `required` + m = construct_type( + value={"type": "modelB", "data": {"type": "a", "data": True}}, + type_=cast(Any, Annotated[Union[ModelA, ModelB], PropertyInfo(discriminator="type")]), + ) + + assert isinstance(m, ModelB) From c76b012c5f5397eb7fbf198cd4720f65182485a4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 17 Mar 2025 13:03:34 +0000 Subject: [PATCH 204/399] fix(ci): ensure pip is always available (#214) --- bin/publish-pypi | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/publish-pypi b/bin/publish-pypi index 05bfccbb..ebebf916 100644 --- a/bin/publish-pypi +++ b/bin/publish-pypi @@ -5,5 +5,6 @@ mkdir -p dist rye build --clean # Patching importlib-metadata version until upstream library version is updated # https://github.com/pypa/twine/issues/977#issuecomment-2189800841 +"$HOME/.rye/self/bin/python3" -m ensurepip "$HOME/.rye/self/bin/python3" -m pip install 'importlib-metadata==7.2.1' rye publish --yes --token=$PYPI_TOKEN From ade2a9976fd0f752cbec45235eab0424775d65b2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 17 Mar 2025 15:51:07 +0000 Subject: [PATCH 205/399] fix(ci): remove publishing patch (#215) --- bin/publish-pypi | 4 ---- pyproject.toml | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/bin/publish-pypi b/bin/publish-pypi index ebebf916..826054e9 100644 --- a/bin/publish-pypi +++ b/bin/publish-pypi @@ -3,8 +3,4 @@ set -eux mkdir -p dist rye build --clean -# Patching importlib-metadata version until upstream library version is updated -# https://github.com/pypa/twine/issues/977#issuecomment-2189800841 -"$HOME/.rye/self/bin/python3" -m ensurepip -"$HOME/.rye/self/bin/python3" -m pip install 'importlib-metadata==7.2.1' rye publish --yes --token=$PYPI_TOKEN diff --git a/pyproject.toml b/pyproject.toml index 19438628..290976bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,7 +86,7 @@ typecheck = { chain = [ "typecheck:mypy" = "mypy ." [build-system] -requires = ["hatchling", "hatch-fancy-pypi-readme"] +requires = ["hatchling==1.26.3", "hatch-fancy-pypi-readme"] build-backend = "hatchling.build" [tool.hatch.build] From 65b29e84c380c7293af425bf06073f39a91976d1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 21 Mar 2025 15:54:22 +0000 Subject: [PATCH 206/399] docs(api): updates to API spec (#216) --- .stats.yml | 2 +- .../types/application_generate_content_params.py | 9 ++++++--- src/writerai/types/applications/job_create_params.py | 9 ++++++--- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.stats.yml b/.stats.yml index 5c701db8..c7093cc2 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 29 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-350e8ec39a0511510d9af6d1ba3e60154d0585a8a8732e50a2fd40ede8881dc1.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-57a138ff1e8940e627dd0108eb14c25bbeacd70e5f1106f8abd796cba6c51882.yml diff --git a/src/writerai/types/application_generate_content_params.py b/src/writerai/types/application_generate_content_params.py index 885a2beb..9541512b 100644 --- a/src/writerai/types/application_generate_content_params.py +++ b/src/writerai/types/application_generate_content_params.py @@ -29,9 +29,12 @@ class Input(TypedDict, total=False): value: Required[List[str]] """The value for the input field. - If file is required you will need to pass a `file_id`. See - [here](https://dev.writer.com/api-guides/api-reference/file-api/upload-files) - for the Files API. + If the input type is "File upload", you must pass the `file_id` of an uploaded + file. You cannot pass a file object directly. See the + [file upload endpoint](/api-guides/api-reference/file-api/upload-files) for + instructions on uploading files or the + [list files endpoint](/api-guides/api-reference/file-api/get-all-files) for how + to see a list of uploaded files and their IDs. """ diff --git a/src/writerai/types/applications/job_create_params.py b/src/writerai/types/applications/job_create_params.py index 86c6b048..467d133a 100644 --- a/src/writerai/types/applications/job_create_params.py +++ b/src/writerai/types/applications/job_create_params.py @@ -25,7 +25,10 @@ class Input(TypedDict, total=False): value: Required[List[str]] """The value for the input field. - If file is required you will need to pass a `file_id`. See - [here](https://dev.writer.com/api-guides/api-reference/file-api/upload-files) - for the Files API. + If the input type is "File upload", you must pass the `file_id` of an uploaded + file. You cannot pass a file object directly. See the + [file upload endpoint](/api-guides/api-reference/file-api/upload-files) for + instructions on uploading files or the + [list files endpoint](/api-guides/api-reference/file-api/get-all-files) for how + to see a list of uploaded files and their IDs. """ From f8b192d1d897b82f34b3dd107a6b4724e3484dbb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 21 Mar 2025 20:16:55 +0000 Subject: [PATCH 207/399] chore(internal): Fix README samples. (#217) --- README.md | 18 +++++++++--------- tests/test_client.py | 8 ++++---- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 6e9c8d07..64512f79 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ client = Writer( chat_completion = client.chat.chat( messages=[ { - "content": "Write a poem about Python", + "content": "Write a haiku about programming", "role": "user", } ], @@ -66,7 +66,7 @@ async def main() -> None: chat_completion = await client.chat.chat( messages=[ { - "content": "Write a poem about Python", + "content": "Write a haiku about programming", "role": "user", } ], @@ -92,7 +92,7 @@ client = Writer() stream = client.chat.chat( messages=[ { - "content": "Write a poem about Python", + "content": "Write a haiku about programming", "role": "user", } ], @@ -113,7 +113,7 @@ client = AsyncWriter() stream = await client.chat.chat( messages=[ { - "content": "Write a poem about Python", + "content": "Write a haiku about programming", "role": "user", } ], @@ -250,7 +250,7 @@ try: client.chat.chat( messages=[ { - "content": "Write a poem about Python", + "content": "Write a haiku about programming", "role": "user", } ], @@ -301,7 +301,7 @@ client = Writer( client.with_options(max_retries=5).chat.chat( messages=[ { - "content": "Write a poem about Python", + "content": "Write a haiku about programming", "role": "user", } ], @@ -332,7 +332,7 @@ client = Writer( client.with_options(timeout=5.0).chat.chat( messages=[ { - "content": "Write a poem about Python", + "content": "Write a haiku about programming", "role": "user", } ], @@ -380,7 +380,7 @@ from writerai import Writer client = Writer() response = client.chat.with_raw_response.chat( messages=[{ - "content": "Write a poem about Python", + "content": "Write a haiku about programming", "role": "user", }], model="palmyra-x-004", @@ -405,7 +405,7 @@ To stream the response body, use `.with_streaming_response` instead, which requi with client.chat.with_streaming_response.chat( messages=[ { - "content": "Write a poem about Python", + "content": "Write a haiku about programming", "role": "user", } ], diff --git a/tests/test_client.py b/tests/test_client.py index 7d9acb8e..5a2f6c16 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -735,7 +735,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> No dict( messages=[ { - "content": "Write a poem about Python", + "content": "Write a haiku about programming", "role": "user", } ], @@ -764,7 +764,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> Non dict( messages=[ { - "content": "Write a poem about Python", + "content": "Write a haiku about programming", "role": "user", } ], @@ -1555,7 +1555,7 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) dict( messages=[ { - "content": "Write a poem about Python", + "content": "Write a haiku about programming", "role": "user", } ], @@ -1584,7 +1584,7 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) dict( messages=[ { - "content": "Write a poem about Python", + "content": "Write a haiku about programming", "role": "user", } ], From de35b7fdc78c8742f248ad3236abe31ce4298e7a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 21 Mar 2025 21:03:08 +0000 Subject: [PATCH 208/399] feat(api): Add Vision endpoint. (#220) --- .stats.yml | 2 +- api.md | 12 ++ src/writerai/_client.py | 10 +- src/writerai/resources/__init__.py | 14 ++ src/writerai/resources/vision.py | 200 ++++++++++++++++++ src/writerai/types/__init__.py | 2 + .../types/vision_analyze_images_params.py | 43 ++++ src/writerai/types/vision_response.py | 11 + tests/api_resources/test_vision.py | 150 +++++++++++++ 9 files changed, 442 insertions(+), 2 deletions(-) create mode 100644 src/writerai/resources/vision.py create mode 100644 src/writerai/types/vision_analyze_images_params.py create mode 100644 src/writerai/types/vision_response.py create mode 100644 tests/api_resources/test_vision.py diff --git a/.stats.yml b/.stats.yml index c7093cc2..c99a6c86 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ -configured_endpoints: 29 +configured_endpoints: 30 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-57a138ff1e8940e627dd0108eb14c25bbeacd70e5f1106f8abd796cba6c51882.yml diff --git a/api.md b/api.md index 2749ea06..9354f894 100644 --- a/api.md +++ b/api.md @@ -181,3 +181,15 @@ from writerai.types.tools import ComprehendMedicalResponse Methods: - client.tools.comprehend.medical(\*\*params) -> ComprehendMedicalResponse + +# Vision + +Types: + +```python +from writerai.types import VisionRequest, VisionResponse +``` + +Methods: + +- client.vision.analyze_images(\*\*params) -> VisionResponse diff --git a/src/writerai/_client.py b/src/writerai/_client.py index d5b8e949..6f359a41 100644 --- a/src/writerai/_client.py +++ b/src/writerai/_client.py @@ -24,7 +24,7 @@ get_async_library, ) from ._version import __version__ -from .resources import chat, files, graphs, models, completions +from .resources import chat, files, graphs, models, vision, completions from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import WriterError, APIStatusError from ._base_client import ( @@ -46,6 +46,7 @@ class Writer(SyncAPIClient): graphs: graphs.GraphsResource files: files.FilesResource tools: tools.ToolsResource + vision: vision.VisionResource with_raw_response: WriterWithRawResponse with_streaming_response: WriterWithStreamedResponse @@ -112,6 +113,7 @@ def __init__( self.graphs = graphs.GraphsResource(self) self.files = files.FilesResource(self) self.tools = tools.ToolsResource(self) + self.vision = vision.VisionResource(self) self.with_raw_response = WriterWithRawResponse(self) self.with_streaming_response = WriterWithStreamedResponse(self) @@ -228,6 +230,7 @@ class AsyncWriter(AsyncAPIClient): graphs: graphs.AsyncGraphsResource files: files.AsyncFilesResource tools: tools.AsyncToolsResource + vision: vision.AsyncVisionResource with_raw_response: AsyncWriterWithRawResponse with_streaming_response: AsyncWriterWithStreamedResponse @@ -294,6 +297,7 @@ def __init__( self.graphs = graphs.AsyncGraphsResource(self) self.files = files.AsyncFilesResource(self) self.tools = tools.AsyncToolsResource(self) + self.vision = vision.AsyncVisionResource(self) self.with_raw_response = AsyncWriterWithRawResponse(self) self.with_streaming_response = AsyncWriterWithStreamedResponse(self) @@ -411,6 +415,7 @@ def __init__(self, client: Writer) -> None: self.graphs = graphs.GraphsResourceWithRawResponse(client.graphs) self.files = files.FilesResourceWithRawResponse(client.files) self.tools = tools.ToolsResourceWithRawResponse(client.tools) + self.vision = vision.VisionResourceWithRawResponse(client.vision) class AsyncWriterWithRawResponse: @@ -422,6 +427,7 @@ def __init__(self, client: AsyncWriter) -> None: self.graphs = graphs.AsyncGraphsResourceWithRawResponse(client.graphs) self.files = files.AsyncFilesResourceWithRawResponse(client.files) self.tools = tools.AsyncToolsResourceWithRawResponse(client.tools) + self.vision = vision.AsyncVisionResourceWithRawResponse(client.vision) class WriterWithStreamedResponse: @@ -433,6 +439,7 @@ def __init__(self, client: Writer) -> None: self.graphs = graphs.GraphsResourceWithStreamingResponse(client.graphs) self.files = files.FilesResourceWithStreamingResponse(client.files) self.tools = tools.ToolsResourceWithStreamingResponse(client.tools) + self.vision = vision.VisionResourceWithStreamingResponse(client.vision) class AsyncWriterWithStreamedResponse: @@ -444,6 +451,7 @@ def __init__(self, client: AsyncWriter) -> None: self.graphs = graphs.AsyncGraphsResourceWithStreamingResponse(client.graphs) self.files = files.AsyncFilesResourceWithStreamingResponse(client.files) self.tools = tools.AsyncToolsResourceWithStreamingResponse(client.tools) + self.vision = vision.AsyncVisionResourceWithStreamingResponse(client.vision) Client = Writer diff --git a/src/writerai/resources/__init__.py b/src/writerai/resources/__init__.py index fc4062c7..40417904 100644 --- a/src/writerai/resources/__init__.py +++ b/src/writerai/resources/__init__.py @@ -40,6 +40,14 @@ ModelsResourceWithStreamingResponse, AsyncModelsResourceWithStreamingResponse, ) +from .vision import ( + VisionResource, + AsyncVisionResource, + VisionResourceWithRawResponse, + AsyncVisionResourceWithRawResponse, + VisionResourceWithStreamingResponse, + AsyncVisionResourceWithStreamingResponse, +) from .completions import ( CompletionsResource, AsyncCompletionsResource, @@ -100,4 +108,10 @@ "AsyncToolsResourceWithRawResponse", "ToolsResourceWithStreamingResponse", "AsyncToolsResourceWithStreamingResponse", + "VisionResource", + "AsyncVisionResource", + "VisionResourceWithRawResponse", + "AsyncVisionResourceWithRawResponse", + "VisionResourceWithStreamingResponse", + "AsyncVisionResourceWithStreamingResponse", ] diff --git a/src/writerai/resources/vision.py b/src/writerai/resources/vision.py new file mode 100644 index 00000000..5317400f --- /dev/null +++ b/src/writerai/resources/vision.py @@ -0,0 +1,200 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Iterable + +import httpx + +from ..types import vision_analyze_images_params +from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from .._utils import ( + maybe_transform, + async_maybe_transform, +) +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.vision_response import VisionResponse + +__all__ = ["VisionResource", "AsyncVisionResource"] + + +class VisionResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> VisionResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers + """ + return VisionResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> VisionResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/writer/writer-python#with_streaming_response + """ + return VisionResourceWithStreamingResponse(self) + + def analyze_images( + self, + *, + model: str, + prompt: str, + variables: Iterable[vision_analyze_images_params.Variable], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> VisionResponse: + """ + Submit images and a prompt to generate an analysis of the images. + + Args: + model: The model to be used for image analysis. Currently only supports + `palmyra-vision`. + + prompt: The prompt to use for the image analysis. The prompt must include the name of + each image variable, surrounded by double curly braces (`{{}}`). For example, + `Describe the difference between the image {{image_1}} and the image {{image_2}}`. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/v1/vision", + body=maybe_transform( + { + "model": model, + "prompt": prompt, + "variables": variables, + }, + vision_analyze_images_params.VisionAnalyzeImagesParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=VisionResponse, + ) + + +class AsyncVisionResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncVisionResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers + """ + return AsyncVisionResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncVisionResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/writer/writer-python#with_streaming_response + """ + return AsyncVisionResourceWithStreamingResponse(self) + + async def analyze_images( + self, + *, + model: str, + prompt: str, + variables: Iterable[vision_analyze_images_params.Variable], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> VisionResponse: + """ + Submit images and a prompt to generate an analysis of the images. + + Args: + model: The model to be used for image analysis. Currently only supports + `palmyra-vision`. + + prompt: The prompt to use for the image analysis. The prompt must include the name of + each image variable, surrounded by double curly braces (`{{}}`). For example, + `Describe the difference between the image {{image_1}} and the image {{image_2}}`. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/v1/vision", + body=await async_maybe_transform( + { + "model": model, + "prompt": prompt, + "variables": variables, + }, + vision_analyze_images_params.VisionAnalyzeImagesParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=VisionResponse, + ) + + +class VisionResourceWithRawResponse: + def __init__(self, vision: VisionResource) -> None: + self._vision = vision + + self.analyze_images = to_raw_response_wrapper( + vision.analyze_images, + ) + + +class AsyncVisionResourceWithRawResponse: + def __init__(self, vision: AsyncVisionResource) -> None: + self._vision = vision + + self.analyze_images = async_to_raw_response_wrapper( + vision.analyze_images, + ) + + +class VisionResourceWithStreamingResponse: + def __init__(self, vision: VisionResource) -> None: + self._vision = vision + + self.analyze_images = to_streamed_response_wrapper( + vision.analyze_images, + ) + + +class AsyncVisionResourceWithStreamingResponse: + def __init__(self, vision: AsyncVisionResource) -> None: + self._vision = vision + + self.analyze_images = async_to_streamed_response_wrapper( + vision.analyze_images, + ) diff --git a/src/writerai/types/__init__.py b/src/writerai/types/__init__.py index e76f9b35..b44e236e 100644 --- a/src/writerai/types/__init__.py +++ b/src/writerai/types/__init__.py @@ -22,6 +22,7 @@ from .question import Question as Question from .completion import Completion as Completion from .chat_completion import ChatCompletion as ChatCompletion +from .vision_response import VisionResponse as VisionResponse from .chat_chat_params import ChatChatParams as ChatChatParams from .completion_chunk import CompletionChunk as CompletionChunk from .file_list_params import FileListParams as FileListParams @@ -47,6 +48,7 @@ from .tool_parse_pdf_response import ToolParsePdfResponse as ToolParsePdfResponse from .completion_create_params import CompletionCreateParams as CompletionCreateParams from .application_list_response import ApplicationListResponse as ApplicationListResponse +from .vision_analyze_images_params import VisionAnalyzeImagesParams as VisionAnalyzeImagesParams from .application_retrieve_response import ApplicationRetrieveResponse as ApplicationRetrieveResponse from .graph_add_file_to_graph_params import GraphAddFileToGraphParams as GraphAddFileToGraphParams from .application_generate_content_chunk import ApplicationGenerateContentChunk as ApplicationGenerateContentChunk diff --git a/src/writerai/types/vision_analyze_images_params.py b/src/writerai/types/vision_analyze_images_params.py new file mode 100644 index 00000000..48cc2b27 --- /dev/null +++ b/src/writerai/types/vision_analyze_images_params.py @@ -0,0 +1,43 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Iterable +from typing_extensions import Required, TypedDict + +__all__ = ["VisionAnalyzeImagesParams", "Variable"] + + +class VisionAnalyzeImagesParams(TypedDict, total=False): + model: Required[str] + """The model to be used for image analysis. + + Currently only supports `palmyra-vision`. + """ + + prompt: Required[str] + """The prompt to use for the image analysis. + + The prompt must include the name of each image variable, surrounded by double + curly braces (`{{}}`). For example, + `Describe the difference between the image {{image_1}} and the image {{image_2}}`. + """ + + variables: Required[Iterable[Variable]] + + +class Variable(TypedDict, total=False): + file_id: Required[str] + """The File ID of the image to be analyzed. + + The file must be uploaded to the Writer platform before it can be used in a + vision request. + """ + + name: Required[str] + """The name of the file variable. + + You must reference this name in the prompt with double curly braces (`{{}}`). + For example, + `Describe the difference between the image {{image_1}} and the image {{image_2}}`. + """ diff --git a/src/writerai/types/vision_response.py b/src/writerai/types/vision_response.py new file mode 100644 index 00000000..0fceb4b0 --- /dev/null +++ b/src/writerai/types/vision_response.py @@ -0,0 +1,11 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + + +from .._models import BaseModel + +__all__ = ["VisionResponse"] + + +class VisionResponse(BaseModel): + data: str + """The result of the image analysis.""" diff --git a/tests/api_resources/test_vision.py b/tests/api_resources/test_vision.py new file mode 100644 index 00000000..1f5d50da --- /dev/null +++ b/tests/api_resources/test_vision.py @@ -0,0 +1,150 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from writerai import Writer, AsyncWriter +from tests.utils import assert_matches_type +from writerai.types import VisionResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestVision: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_analyze_images(self, client: Writer) -> None: + vision = client.vision.analyze_images( + model="palmyra-vision", + prompt="Describe the difference between the image {{image_1}} and the image {{image_2}}.", + variables=[ + { + "file_id": "f1234", + "name": "image_1", + }, + { + "file_id": "f9876", + "name": "image_2", + }, + ], + ) + assert_matches_type(VisionResponse, vision, path=["response"]) + + @parametrize + def test_raw_response_analyze_images(self, client: Writer) -> None: + response = client.vision.with_raw_response.analyze_images( + model="palmyra-vision", + prompt="Describe the difference between the image {{image_1}} and the image {{image_2}}.", + variables=[ + { + "file_id": "f1234", + "name": "image_1", + }, + { + "file_id": "f9876", + "name": "image_2", + }, + ], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + vision = response.parse() + assert_matches_type(VisionResponse, vision, path=["response"]) + + @parametrize + def test_streaming_response_analyze_images(self, client: Writer) -> None: + with client.vision.with_streaming_response.analyze_images( + model="palmyra-vision", + prompt="Describe the difference between the image {{image_1}} and the image {{image_2}}.", + variables=[ + { + "file_id": "f1234", + "name": "image_1", + }, + { + "file_id": "f9876", + "name": "image_2", + }, + ], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + vision = response.parse() + assert_matches_type(VisionResponse, vision, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncVision: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_analyze_images(self, async_client: AsyncWriter) -> None: + vision = await async_client.vision.analyze_images( + model="palmyra-vision", + prompt="Describe the difference between the image {{image_1}} and the image {{image_2}}.", + variables=[ + { + "file_id": "f1234", + "name": "image_1", + }, + { + "file_id": "f9876", + "name": "image_2", + }, + ], + ) + assert_matches_type(VisionResponse, vision, path=["response"]) + + @parametrize + async def test_raw_response_analyze_images(self, async_client: AsyncWriter) -> None: + response = await async_client.vision.with_raw_response.analyze_images( + model="palmyra-vision", + prompt="Describe the difference between the image {{image_1}} and the image {{image_2}}.", + variables=[ + { + "file_id": "f1234", + "name": "image_1", + }, + { + "file_id": "f9876", + "name": "image_2", + }, + ], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + vision = await response.parse() + assert_matches_type(VisionResponse, vision, path=["response"]) + + @parametrize + async def test_streaming_response_analyze_images(self, async_client: AsyncWriter) -> None: + async with async_client.vision.with_streaming_response.analyze_images( + model="palmyra-vision", + prompt="Describe the difference between the image {{image_1}} and the image {{image_2}}.", + variables=[ + { + "file_id": "f1234", + "name": "image_1", + }, + { + "file_id": "f9876", + "name": "image_2", + }, + ], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + vision = await response.parse() + assert_matches_type(VisionResponse, vision, path=["response"]) + + assert cast(Any, response.is_closed) is True From 452a6b7aab20be4ba686ee0333025e7b8aa54134 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 21 Mar 2025 22:53:27 +0000 Subject: [PATCH 209/399] feat(api): Add Vision endpoint. (#221) --- api.md | 2 +- src/writerai/resources/vision.py | 30 +++++++++---------- src/writerai/types/__init__.py | 2 +- ...ges_params.py => vision_analyze_params.py} | 4 +-- tests/api_resources/test_vision.py | 24 +++++++-------- 5 files changed, 31 insertions(+), 31 deletions(-) rename src/writerai/types/{vision_analyze_images_params.py => vision_analyze_params.py} (91%) diff --git a/api.md b/api.md index 9354f894..5cb0e727 100644 --- a/api.md +++ b/api.md @@ -192,4 +192,4 @@ from writerai.types import VisionRequest, VisionResponse Methods: -- client.vision.analyze_images(\*\*params) -> VisionResponse +- client.vision.analyze(\*\*params) -> VisionResponse diff --git a/src/writerai/resources/vision.py b/src/writerai/resources/vision.py index 5317400f..26975788 100644 --- a/src/writerai/resources/vision.py +++ b/src/writerai/resources/vision.py @@ -6,7 +6,7 @@ import httpx -from ..types import vision_analyze_images_params +from ..types import vision_analyze_params from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven from .._utils import ( maybe_transform, @@ -46,12 +46,12 @@ def with_streaming_response(self) -> VisionResourceWithStreamingResponse: """ return VisionResourceWithStreamingResponse(self) - def analyze_images( + def analyze( self, *, model: str, prompt: str, - variables: Iterable[vision_analyze_images_params.Variable], + variables: Iterable[vision_analyze_params.Variable], # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -86,7 +86,7 @@ def analyze_images( "prompt": prompt, "variables": variables, }, - vision_analyze_images_params.VisionAnalyzeImagesParams, + vision_analyze_params.VisionAnalyzeParams, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -115,12 +115,12 @@ def with_streaming_response(self) -> AsyncVisionResourceWithStreamingResponse: """ return AsyncVisionResourceWithStreamingResponse(self) - async def analyze_images( + async def analyze( self, *, model: str, prompt: str, - variables: Iterable[vision_analyze_images_params.Variable], + variables: Iterable[vision_analyze_params.Variable], # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -155,7 +155,7 @@ async def analyze_images( "prompt": prompt, "variables": variables, }, - vision_analyze_images_params.VisionAnalyzeImagesParams, + vision_analyze_params.VisionAnalyzeParams, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -168,8 +168,8 @@ class VisionResourceWithRawResponse: def __init__(self, vision: VisionResource) -> None: self._vision = vision - self.analyze_images = to_raw_response_wrapper( - vision.analyze_images, + self.analyze = to_raw_response_wrapper( + vision.analyze, ) @@ -177,8 +177,8 @@ class AsyncVisionResourceWithRawResponse: def __init__(self, vision: AsyncVisionResource) -> None: self._vision = vision - self.analyze_images = async_to_raw_response_wrapper( - vision.analyze_images, + self.analyze = async_to_raw_response_wrapper( + vision.analyze, ) @@ -186,8 +186,8 @@ class VisionResourceWithStreamingResponse: def __init__(self, vision: VisionResource) -> None: self._vision = vision - self.analyze_images = to_streamed_response_wrapper( - vision.analyze_images, + self.analyze = to_streamed_response_wrapper( + vision.analyze, ) @@ -195,6 +195,6 @@ class AsyncVisionResourceWithStreamingResponse: def __init__(self, vision: AsyncVisionResource) -> None: self._vision = vision - self.analyze_images = async_to_streamed_response_wrapper( - vision.analyze_images, + self.analyze = async_to_streamed_response_wrapper( + vision.analyze, ) diff --git a/src/writerai/types/__init__.py b/src/writerai/types/__init__.py index b44e236e..03f63434 100644 --- a/src/writerai/types/__init__.py +++ b/src/writerai/types/__init__.py @@ -41,6 +41,7 @@ from .graph_question_params import GraphQuestionParams as GraphQuestionParams from .graph_update_response import GraphUpdateResponse as GraphUpdateResponse from .tool_parse_pdf_params import ToolParsePdfParams as ToolParsePdfParams +from .vision_analyze_params import VisionAnalyzeParams as VisionAnalyzeParams from .chat_completion_choice import ChatCompletionChoice as ChatCompletionChoice from .application_list_params import ApplicationListParams as ApplicationListParams from .chat_completion_message import ChatCompletionMessage as ChatCompletionMessage @@ -48,7 +49,6 @@ from .tool_parse_pdf_response import ToolParsePdfResponse as ToolParsePdfResponse from .completion_create_params import CompletionCreateParams as CompletionCreateParams from .application_list_response import ApplicationListResponse as ApplicationListResponse -from .vision_analyze_images_params import VisionAnalyzeImagesParams as VisionAnalyzeImagesParams from .application_retrieve_response import ApplicationRetrieveResponse as ApplicationRetrieveResponse from .graph_add_file_to_graph_params import GraphAddFileToGraphParams as GraphAddFileToGraphParams from .application_generate_content_chunk import ApplicationGenerateContentChunk as ApplicationGenerateContentChunk diff --git a/src/writerai/types/vision_analyze_images_params.py b/src/writerai/types/vision_analyze_params.py similarity index 91% rename from src/writerai/types/vision_analyze_images_params.py rename to src/writerai/types/vision_analyze_params.py index 48cc2b27..96b50b99 100644 --- a/src/writerai/types/vision_analyze_images_params.py +++ b/src/writerai/types/vision_analyze_params.py @@ -5,10 +5,10 @@ from typing import Iterable from typing_extensions import Required, TypedDict -__all__ = ["VisionAnalyzeImagesParams", "Variable"] +__all__ = ["VisionAnalyzeParams", "Variable"] -class VisionAnalyzeImagesParams(TypedDict, total=False): +class VisionAnalyzeParams(TypedDict, total=False): model: Required[str] """The model to be used for image analysis. diff --git a/tests/api_resources/test_vision.py b/tests/api_resources/test_vision.py index 1f5d50da..20f5d72f 100644 --- a/tests/api_resources/test_vision.py +++ b/tests/api_resources/test_vision.py @@ -18,8 +18,8 @@ class TestVision: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) @parametrize - def test_method_analyze_images(self, client: Writer) -> None: - vision = client.vision.analyze_images( + def test_method_analyze(self, client: Writer) -> None: + vision = client.vision.analyze( model="palmyra-vision", prompt="Describe the difference between the image {{image_1}} and the image {{image_2}}.", variables=[ @@ -36,8 +36,8 @@ def test_method_analyze_images(self, client: Writer) -> None: assert_matches_type(VisionResponse, vision, path=["response"]) @parametrize - def test_raw_response_analyze_images(self, client: Writer) -> None: - response = client.vision.with_raw_response.analyze_images( + def test_raw_response_analyze(self, client: Writer) -> None: + response = client.vision.with_raw_response.analyze( model="palmyra-vision", prompt="Describe the difference between the image {{image_1}} and the image {{image_2}}.", variables=[ @@ -58,8 +58,8 @@ def test_raw_response_analyze_images(self, client: Writer) -> None: assert_matches_type(VisionResponse, vision, path=["response"]) @parametrize - def test_streaming_response_analyze_images(self, client: Writer) -> None: - with client.vision.with_streaming_response.analyze_images( + def test_streaming_response_analyze(self, client: Writer) -> None: + with client.vision.with_streaming_response.analyze( model="palmyra-vision", prompt="Describe the difference between the image {{image_1}} and the image {{image_2}}.", variables=[ @@ -86,8 +86,8 @@ class TestAsyncVision: parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) @parametrize - async def test_method_analyze_images(self, async_client: AsyncWriter) -> None: - vision = await async_client.vision.analyze_images( + async def test_method_analyze(self, async_client: AsyncWriter) -> None: + vision = await async_client.vision.analyze( model="palmyra-vision", prompt="Describe the difference between the image {{image_1}} and the image {{image_2}}.", variables=[ @@ -104,8 +104,8 @@ async def test_method_analyze_images(self, async_client: AsyncWriter) -> None: assert_matches_type(VisionResponse, vision, path=["response"]) @parametrize - async def test_raw_response_analyze_images(self, async_client: AsyncWriter) -> None: - response = await async_client.vision.with_raw_response.analyze_images( + async def test_raw_response_analyze(self, async_client: AsyncWriter) -> None: + response = await async_client.vision.with_raw_response.analyze( model="palmyra-vision", prompt="Describe the difference between the image {{image_1}} and the image {{image_2}}.", variables=[ @@ -126,8 +126,8 @@ async def test_raw_response_analyze_images(self, async_client: AsyncWriter) -> N assert_matches_type(VisionResponse, vision, path=["response"]) @parametrize - async def test_streaming_response_analyze_images(self, async_client: AsyncWriter) -> None: - async with async_client.vision.with_streaming_response.analyze_images( + async def test_streaming_response_analyze(self, async_client: AsyncWriter) -> None: + async with async_client.vision.with_streaming_response.analyze( model="palmyra-vision", prompt="Describe the difference between the image {{image_1}} and the image {{image_2}}.", variables=[ From 99c17992a2735c1343594fe7ba4a5c520a157e40 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 26 Mar 2025 16:35:24 +0000 Subject: [PATCH 210/399] chore: fix typos (#222) --- src/writerai/_models.py | 2 +- src/writerai/_utils/_transform.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/writerai/_models.py b/src/writerai/_models.py index b51a1bf5..34935716 100644 --- a/src/writerai/_models.py +++ b/src/writerai/_models.py @@ -681,7 +681,7 @@ def set_pydantic_config(typ: Any, config: pydantic.ConfigDict) -> None: setattr(typ, "__pydantic_config__", config) # noqa: B010 -# our use of subclasssing here causes weirdness for type checkers, +# our use of subclassing here causes weirdness for type checkers, # so we just pretend that we don't subclass if TYPE_CHECKING: GenericModel = BaseModel diff --git a/src/writerai/_utils/_transform.py b/src/writerai/_utils/_transform.py index 18afd9d8..7ac2e17f 100644 --- a/src/writerai/_utils/_transform.py +++ b/src/writerai/_utils/_transform.py @@ -126,7 +126,7 @@ def _get_annotated_type(type_: type) -> type | None: def _maybe_transform_key(key: str, type_: type) -> str: """Transform the given `data` based on the annotations provided in `type_`. - Note: this function only looks at `Annotated` types that contain `PropertInfo` metadata. + Note: this function only looks at `Annotated` types that contain `PropertyInfo` metadata. """ annotated_type = _get_annotated_type(type_) if annotated_type is None: From 43efdebcd5c40608cb0615e583d1788a9d63cb0c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 26 Mar 2025 17:17:49 +0000 Subject: [PATCH 211/399] docs(api): updates to API spec (#223) --- .stats.yml | 2 +- src/writerai/resources/chat.py | 48 +++++++++++------- src/writerai/types/chat_chat_params.py | 8 +-- src/writerai/types/shared/tool_param.py | 49 ++++++++++++++++++- .../types/shared_params/tool_param.py | 49 +++++++++++++++++-- 5 files changed, 129 insertions(+), 27 deletions(-) diff --git a/.stats.yml b/.stats.yml index c99a6c86..96f27d2c 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 30 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-57a138ff1e8940e627dd0108eb14c25bbeacd70e5f1106f8abd796cba6c51882.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-50d65469557b50b46f15db6f5da3b81ed5c1f551e5eab64ded93e46eafaa3696.yml diff --git a/src/writerai/resources/chat.py b/src/writerai/resources/chat.py index 4edd3bef..cd7ac385 100644 --- a/src/writerai/resources/chat.py +++ b/src/writerai/resources/chat.py @@ -115,9 +115,11 @@ def chat( automatically choose the best tool, `none` disables tool calling. You can also pass a specific previously defined function. - tools: An array of tools described to the model using JSON schema that the model can - use to generate responses. You can define your own functions or use the built-in - `graph` or `llm` tools. + tools: An array containing tool definitions for tools that the model can use to + generate responses. The tool definitions use JSON schema. You can define your + own functions or use one of the built-in `graph`, `llm`, or `vision` tools. Note + that you can only use one built-in tool type in the array (only one of `graph`, + `llm`, or `vision`). top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with @@ -198,9 +200,11 @@ def chat( automatically choose the best tool, `none` disables tool calling. You can also pass a specific previously defined function. - tools: An array of tools described to the model using JSON schema that the model can - use to generate responses. You can define your own functions or use the built-in - `graph` or `llm` tools. + tools: An array containing tool definitions for tools that the model can use to + generate responses. The tool definitions use JSON schema. You can define your + own functions or use one of the built-in `graph`, `llm`, or `vision` tools. Note + that you can only use one built-in tool type in the array (only one of `graph`, + `llm`, or `vision`). top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with @@ -281,9 +285,11 @@ def chat( automatically choose the best tool, `none` disables tool calling. You can also pass a specific previously defined function. - tools: An array of tools described to the model using JSON schema that the model can - use to generate responses. You can define your own functions or use the built-in - `graph` or `llm` tools. + tools: An array containing tool definitions for tools that the model can use to + generate responses. The tool definitions use JSON schema. You can define your + own functions or use one of the built-in `graph`, `llm`, or `vision` tools. Note + that you can only use one built-in tool type in the array (only one of `graph`, + `llm`, or `vision`). top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with @@ -435,9 +441,11 @@ async def chat( automatically choose the best tool, `none` disables tool calling. You can also pass a specific previously defined function. - tools: An array of tools described to the model using JSON schema that the model can - use to generate responses. You can define your own functions or use the built-in - `graph` or `llm` tools. + tools: An array containing tool definitions for tools that the model can use to + generate responses. The tool definitions use JSON schema. You can define your + own functions or use one of the built-in `graph`, `llm`, or `vision` tools. Note + that you can only use one built-in tool type in the array (only one of `graph`, + `llm`, or `vision`). top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with @@ -518,9 +526,11 @@ async def chat( automatically choose the best tool, `none` disables tool calling. You can also pass a specific previously defined function. - tools: An array of tools described to the model using JSON schema that the model can - use to generate responses. You can define your own functions or use the built-in - `graph` or `llm` tools. + tools: An array containing tool definitions for tools that the model can use to + generate responses. The tool definitions use JSON schema. You can define your + own functions or use one of the built-in `graph`, `llm`, or `vision` tools. Note + that you can only use one built-in tool type in the array (only one of `graph`, + `llm`, or `vision`). top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with @@ -601,9 +611,11 @@ async def chat( automatically choose the best tool, `none` disables tool calling. You can also pass a specific previously defined function. - tools: An array of tools described to the model using JSON schema that the model can - use to generate responses. You can define your own functions or use the built-in - `graph` or `llm` tools. + tools: An array containing tool definitions for tools that the model can use to + generate responses. The tool definitions use JSON schema. You can define your + own functions or use one of the built-in `graph`, `llm`, or `vision` tools. Note + that you can only use one built-in tool type in the array (only one of `graph`, + `llm`, or `vision`). top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with diff --git a/src/writerai/types/chat_chat_params.py b/src/writerai/types/chat_chat_params.py index 1b14328b..98a6a2fe 100644 --- a/src/writerai/types/chat_chat_params.py +++ b/src/writerai/types/chat_chat_params.py @@ -77,9 +77,11 @@ class ChatChatParamsBase(TypedDict, total=False): tools: Iterable[ToolParam] """ - An array of tools described to the model using JSON schema that the model can - use to generate responses. You can define your own functions or use the built-in - `graph` or `llm` tools. + An array containing tool definitions for tools that the model can use to + generate responses. The tool definitions use JSON schema. You can define your + own functions or use one of the built-in `graph`, `llm`, or `vision` tools. Note + that you can only use one built-in tool type in the array (only one of `graph`, + `llm`, or `vision`). """ top_p: float diff --git a/src/writerai/types/shared/tool_param.py b/src/writerai/types/shared/tool_param.py index addecc6e..ccc6cd45 100644 --- a/src/writerai/types/shared/tool_param.py +++ b/src/writerai/types/shared/tool_param.py @@ -7,7 +7,17 @@ from ..._models import BaseModel from .function_definition import FunctionDefinition -__all__ = ["ToolParam", "FunctionTool", "GraphTool", "GraphToolFunction", "LlmTool", "LlmToolFunction"] +__all__ = [ + "ToolParam", + "FunctionTool", + "GraphTool", + "GraphToolFunction", + "LlmTool", + "LlmToolFunction", + "VisionTool", + "VisionToolFunction", + "VisionToolFunctionVariable", +] class FunctionTool(BaseModel): @@ -53,4 +63,39 @@ class LlmTool(BaseModel): """The type of tool.""" -ToolParam: TypeAlias = Annotated[Union[FunctionTool, GraphTool, LlmTool], PropertyInfo(discriminator="type")] +class VisionToolFunctionVariable(BaseModel): + file_id: str + """The File ID of the image to be analyzed. + + The file must be uploaded to the Writer platform before you use it with the + Vision tool. + """ + + name: str + """The name of the file variable. + + You must reference this name in the `message.content` field of the request to + the chat completions endpoint. Use double curly braces (`{{}}`) to reference the + file. For example, + `Describe the difference between the image {{image_1}} and the image {{image_2}}`. + """ + + +class VisionToolFunction(BaseModel): + model: str + """The model to be used for image analysis. Must be `palmyra-vision`.""" + + variables: List[VisionToolFunctionVariable] + + +class VisionTool(BaseModel): + function: VisionToolFunction + """A tool that uses Palmyra Vision to analyze images.""" + + type: Literal["vision"] + """The type of tool.""" + + +ToolParam: TypeAlias = Annotated[ + Union[FunctionTool, GraphTool, LlmTool, VisionTool], PropertyInfo(discriminator="type") +] diff --git a/src/writerai/types/shared_params/tool_param.py b/src/writerai/types/shared_params/tool_param.py index fcb3f373..7723c2f3 100644 --- a/src/writerai/types/shared_params/tool_param.py +++ b/src/writerai/types/shared_params/tool_param.py @@ -2,12 +2,22 @@ from __future__ import annotations -from typing import List, Union +from typing import List, Union, Iterable from typing_extensions import Literal, Required, TypeAlias, TypedDict from .function_definition import FunctionDefinition -__all__ = ["ToolParam", "FunctionTool", "GraphTool", "GraphToolFunction", "LlmTool", "LlmToolFunction"] +__all__ = [ + "ToolParam", + "FunctionTool", + "GraphTool", + "GraphToolFunction", + "LlmTool", + "LlmToolFunction", + "VisionTool", + "VisionToolFunction", + "VisionToolFunctionVariable", +] class FunctionTool(TypedDict, total=False): @@ -53,4 +63,37 @@ class LlmTool(TypedDict, total=False): """The type of tool.""" -ToolParam: TypeAlias = Union[FunctionTool, GraphTool, LlmTool] +class VisionToolFunctionVariable(TypedDict, total=False): + file_id: Required[str] + """The File ID of the image to be analyzed. + + The file must be uploaded to the Writer platform before you use it with the + Vision tool. + """ + + name: Required[str] + """The name of the file variable. + + You must reference this name in the `message.content` field of the request to + the chat completions endpoint. Use double curly braces (`{{}}`) to reference the + file. For example, + `Describe the difference between the image {{image_1}} and the image {{image_2}}`. + """ + + +class VisionToolFunction(TypedDict, total=False): + model: Required[str] + """The model to be used for image analysis. Must be `palmyra-vision`.""" + + variables: Required[Iterable[VisionToolFunctionVariable]] + + +class VisionTool(TypedDict, total=False): + function: Required[VisionToolFunction] + """A tool that uses Palmyra Vision to analyze images.""" + + type: Required[Literal["vision"]] + """The type of tool.""" + + +ToolParam: TypeAlias = Union[FunctionTool, GraphTool, LlmTool, VisionTool] From c62dc412a96727adebe5bba659f6e22fe935e50d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 26 Mar 2025 17:31:52 +0000 Subject: [PATCH 212/399] codegen metadata --- .stats.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.stats.yml b/.stats.yml index 96f27d2c..400e359c 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,4 @@ configured_endpoints: 30 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-50d65469557b50b46f15db6f5da3b81ed5c1f551e5eab64ded93e46eafaa3696.yml +openapi_spec_hash: 5e65cdf348fa909363010cd2fb5866c5 +config_hash: b3310cd2944d74a3599e847847226a42 From c0ee901a9835b18a9d7927ef4af0f24f979ae2c9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 26 Mar 2025 21:16:10 +0000 Subject: [PATCH 213/399] docs(api): updates to API spec (#224) --- .stats.yml | 4 +- src/writerai/resources/graphs.py | 28 ++++++------- src/writerai/types/chat_chat_params.py | 6 +++ src/writerai/types/graph_question_params.py | 4 +- tests/api_resources/test_graphs.py | 46 +++++++++++++++------ 5 files changed, 58 insertions(+), 30 deletions(-) diff --git a/.stats.yml b/.stats.yml index 400e359c..79dab344 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-50d65469557b50b46f15db6f5da3b81ed5c1f551e5eab64ded93e46eafaa3696.yml -openapi_spec_hash: 5e65cdf348fa909363010cd2fb5866c5 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-d15316b8a3a086ae9ec8eea0d436b0885262df9bcf23b9587ad059e50357c220.yml +openapi_spec_hash: 4f81a4f4840438f80eff345e76ead962 config_hash: b3310cd2944d74a3599e847847226a42 diff --git a/src/writerai/resources/graphs.py b/src/writerai/resources/graphs.py index 1833f762..a3a044f5 100644 --- a/src/writerai/resources/graphs.py +++ b/src/writerai/resources/graphs.py @@ -324,8 +324,8 @@ def question( *, graph_ids: List[str], question: str, - stream: Literal[False], - subqueries: bool, + stream: Literal[False] | NotGiven = NOT_GIVEN, + subqueries: bool | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -364,7 +364,7 @@ def question( graph_ids: List[str], question: str, stream: Literal[True], - subqueries: bool, + subqueries: bool | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -403,7 +403,7 @@ def question( graph_ids: List[str], question: str, stream: bool, - subqueries: bool, + subqueries: bool | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -435,14 +435,14 @@ def question( """ ... - @required_args(["graph_ids", "question", "stream", "subqueries"]) + @required_args(["graph_ids", "question"], ["graph_ids", "question", "stream"]) def question( self, *, graph_ids: List[str], question: str, - stream: Literal[False] | Literal[True], - subqueries: bool, + stream: Literal[False] | Literal[True] | NotGiven = NOT_GIVEN, + subqueries: bool | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -789,8 +789,8 @@ async def question( *, graph_ids: List[str], question: str, - stream: Literal[False], - subqueries: bool, + stream: Literal[False] | NotGiven = NOT_GIVEN, + subqueries: bool | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -829,7 +829,7 @@ async def question( graph_ids: List[str], question: str, stream: Literal[True], - subqueries: bool, + subqueries: bool | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -868,7 +868,7 @@ async def question( graph_ids: List[str], question: str, stream: bool, - subqueries: bool, + subqueries: bool | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -900,14 +900,14 @@ async def question( """ ... - @required_args(["graph_ids", "question", "stream", "subqueries"]) + @required_args(["graph_ids", "question"], ["graph_ids", "question", "stream"]) async def question( self, *, graph_ids: List[str], question: str, - stream: Literal[False] | Literal[True], - subqueries: bool, + stream: Literal[False] | Literal[True] | NotGiven = NOT_GIVEN, + subqueries: bool | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, diff --git a/src/writerai/types/chat_chat_params.py b/src/writerai/types/chat_chat_params.py index 98a6a2fe..08dce8f2 100644 --- a/src/writerai/types/chat_chat_params.py +++ b/src/writerai/types/chat_chat_params.py @@ -95,6 +95,12 @@ class ChatChatParamsBase(TypedDict, total=False): class Message(TypedDict, total=False): role: Required[Literal["user", "assistant", "system", "tool"]] + """The role of the chat message. + + You can provide a system prompt by setting the role to `system`, or specify that + a message is the result of a [tool call](/api-guides/tool-calling) by setting + the role to `tool`. + """ content: Optional[str] diff --git a/src/writerai/types/graph_question_params.py b/src/writerai/types/graph_question_params.py index 508d10ae..570cbbd6 100644 --- a/src/writerai/types/graph_question_params.py +++ b/src/writerai/types/graph_question_params.py @@ -15,12 +15,12 @@ class GraphQuestionParamsBase(TypedDict, total=False): question: Required[str] """The question to be answered using the Knowledge Graph.""" - subqueries: Required[bool] + subqueries: bool """Specify whether to include subqueries.""" class GraphQuestionParamsNonStreaming(GraphQuestionParamsBase, total=False): - stream: Required[Literal[False]] + stream: Literal[False] """Determines whether the model's output should be streamed. If true, the output is generated and sent incrementally, which can be useful for diff --git a/tests/api_resources/test_graphs.py b/tests/api_resources/test_graphs.py index 510af9e6..22c7b21b 100644 --- a/tests/api_resources/test_graphs.py +++ b/tests/api_resources/test_graphs.py @@ -261,6 +261,14 @@ def test_path_params_add_file_to_graph(self, client: Writer) -> None: @parametrize def test_method_question_overload_1(self, client: Writer) -> None: + graph = client.graphs.question( + graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], + question="question", + ) + assert_matches_type(Question, graph, path=["response"]) + + @parametrize + def test_method_question_with_all_params_overload_1(self, client: Writer) -> None: graph = client.graphs.question( graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], question="question", @@ -274,8 +282,6 @@ def test_raw_response_question_overload_1(self, client: Writer) -> None: response = client.graphs.with_raw_response.question( graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], question="question", - stream=False, - subqueries=True, ) assert response.is_closed is True @@ -288,8 +294,6 @@ def test_streaming_response_question_overload_1(self, client: Writer) -> None: with client.graphs.with_streaming_response.question( graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], question="question", - stream=False, - subqueries=True, ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -301,6 +305,15 @@ def test_streaming_response_question_overload_1(self, client: Writer) -> None: @parametrize def test_method_question_overload_2(self, client: Writer) -> None: + graph_stream = client.graphs.question( + graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], + question="question", + stream=True, + ) + graph_stream.response.close() + + @parametrize + def test_method_question_with_all_params_overload_2(self, client: Writer) -> None: graph_stream = client.graphs.question( graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], question="question", @@ -315,7 +328,6 @@ def test_raw_response_question_overload_2(self, client: Writer) -> None: graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], question="question", stream=True, - subqueries=True, ) assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -328,7 +340,6 @@ def test_streaming_response_question_overload_2(self, client: Writer) -> None: graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], question="question", stream=True, - subqueries=True, ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -625,6 +636,14 @@ async def test_path_params_add_file_to_graph(self, async_client: AsyncWriter) -> @parametrize async def test_method_question_overload_1(self, async_client: AsyncWriter) -> None: + graph = await async_client.graphs.question( + graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], + question="question", + ) + assert_matches_type(Question, graph, path=["response"]) + + @parametrize + async def test_method_question_with_all_params_overload_1(self, async_client: AsyncWriter) -> None: graph = await async_client.graphs.question( graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], question="question", @@ -638,8 +657,6 @@ async def test_raw_response_question_overload_1(self, async_client: AsyncWriter) response = await async_client.graphs.with_raw_response.question( graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], question="question", - stream=False, - subqueries=True, ) assert response.is_closed is True @@ -652,8 +669,6 @@ async def test_streaming_response_question_overload_1(self, async_client: AsyncW async with async_client.graphs.with_streaming_response.question( graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], question="question", - stream=False, - subqueries=True, ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -665,6 +680,15 @@ async def test_streaming_response_question_overload_1(self, async_client: AsyncW @parametrize async def test_method_question_overload_2(self, async_client: AsyncWriter) -> None: + graph_stream = await async_client.graphs.question( + graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], + question="question", + stream=True, + ) + await graph_stream.response.aclose() + + @parametrize + async def test_method_question_with_all_params_overload_2(self, async_client: AsyncWriter) -> None: graph_stream = await async_client.graphs.question( graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], question="question", @@ -679,7 +703,6 @@ async def test_raw_response_question_overload_2(self, async_client: AsyncWriter) graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], question="question", stream=True, - subqueries=True, ) assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -692,7 +715,6 @@ async def test_streaming_response_question_overload_2(self, async_client: AsyncW graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], question="question", stream=True, - subqueries=True, ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" From 32c932ba41dd301621de1b5e2bae1a1b8f901773 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 1 Apr 2025 21:51:47 +0000 Subject: [PATCH 214/399] chore(internal): version bump (#225) --- .release-please-manifest.json | 2 +- README.md | 2 +- pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 65f558e7..2c7c583c 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.0.0" + ".": "2.1.0-rc1" } \ No newline at end of file diff --git a/README.md b/README.md index 64512f79..0d1b33c7 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The REST API documentation can be found on [dev.writer.com](https://dev.writer.c ```sh # install from PyPI -pip install writer-sdk +pip install --pre writer-sdk ``` ## Usage diff --git a/pyproject.toml b/pyproject.toml index 290976bb..c40ba726 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "2.0.0" +version = "2.1.0-rc1" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index a7140c47..ff36ac1c 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "2.0.0" # x-release-please-version +__version__ = "2.1.0-rc1" # x-release-please-version From 78bb6a9cad88a37ff45c793d31e0cf20f4d16521 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 2 Apr 2025 16:38:29 +0000 Subject: [PATCH 215/399] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 79dab344..cda70cf6 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-d15316b8a3a086ae9ec8eea0d436b0885262df9bcf23b9587ad059e50357c220.yml openapi_spec_hash: 4f81a4f4840438f80eff345e76ead962 -config_hash: b3310cd2944d74a3599e847847226a42 +config_hash: 8b4e4a902369723a665b1d265169a3f1 From c9a5d8335077a9d4b60650d0d3d296a2b774ce23 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 3 Apr 2025 15:54:27 +0000 Subject: [PATCH 216/399] chore(internal): remove trailing character (#227) --- tests/test_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_client.py b/tests/test_client.py index 5a2f6c16..c43a200d 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1695,7 +1695,7 @@ def test_get_platform(self) -> None: import threading from writerai._utils import asyncify - from writerai._base_client import get_platform + from writerai._base_client import get_platform async def test_main() -> None: result = await asyncify(get_platform)() From 37d664a8fb1002a992cbbb401ddefd0e42e9e801 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 4 Apr 2025 16:22:43 +0000 Subject: [PATCH 217/399] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index cda70cf6..12896beb 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-d15316b8a3a086ae9ec8eea0d436b0885262df9bcf23b9587ad059e50357c220.yml openapi_spec_hash: 4f81a4f4840438f80eff345e76ead962 -config_hash: 8b4e4a902369723a665b1d265169a3f1 +config_hash: ee2e5a914fd298a6f4c740f5ce187f87 From e5eb171fb11dc88096228f36e86451fc92a1d9c6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 4 Apr 2025 20:55:29 +0000 Subject: [PATCH 218/399] chore(internal): version bump (#229) --- .release-please-manifest.json | 2 +- README.md | 2 +- pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 2c7c583c..656a2ef1 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.1.0-rc1" + ".": "2.1.0" } \ No newline at end of file diff --git a/README.md b/README.md index 0d1b33c7..64512f79 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The REST API documentation can be found on [dev.writer.com](https://dev.writer.c ```sh # install from PyPI -pip install --pre writer-sdk +pip install writer-sdk ``` ## Usage diff --git a/pyproject.toml b/pyproject.toml index c40ba726..78fbdac7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "2.1.0-rc1" +version = "2.1.0" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index ff36ac1c..4443c0f6 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "2.1.0-rc1" # x-release-please-version +__version__ = "2.1.0" # x-release-please-version From 06c3c389cf5c40f0e4327b543447c0e2c3faee90 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 4 Apr 2025 20:55:38 +0000 Subject: [PATCH 219/399] chore(internal): codegen related update (#230) From 51c0c2f48ab032f4fb97af99684380c6f5aa1329 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 4 Apr 2025 21:09:09 +0000 Subject: [PATCH 220/399] docs: swap examples used in readme (#231) --- README.md | 18 ------------------ src/writerai/_files.py | 2 +- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/README.md b/README.md index 64512f79..f3d1b275 100644 --- a/README.md +++ b/README.md @@ -213,24 +213,6 @@ chat_completion = client.chat.chat( print(chat_completion.stream_options) ``` -## File uploads - -Request parameters that correspond to file uploads can be passed as `bytes`, a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance or a tuple of `(filename, contents, media type)`. - -```python -from pathlib import Path -from writerai import Writer - -client = Writer() - -client.files.upload( - content=Path("/path/to/file"), - content_disposition="Content-Disposition", -) -``` - -The async client uses the exact same interface. If you pass a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance, the file contents will be read asynchronously automatically. - ## Handling errors When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `writerai.APIConnectionError` is raised. diff --git a/src/writerai/_files.py b/src/writerai/_files.py index f6f78ade..715cc207 100644 --- a/src/writerai/_files.py +++ b/src/writerai/_files.py @@ -34,7 +34,7 @@ def assert_is_file_content(obj: object, *, key: str | None = None) -> None: if not is_file_content(obj): prefix = f"Expected entry at `{key}`" if key is not None else f"Expected file input `{obj!r}`" raise RuntimeError( - f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead. See https://github.com/writer/writer-python/tree/main#file-uploads" + f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead." ) from None From cecce6d1c2081113788b2ba2472ab60787670fd1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 12:09:27 +0000 Subject: [PATCH 221/399] chore(internal): slight transform perf improvement (#233) --- src/writerai/_utils/_transform.py | 22 ++++++++++++++++++++++ tests/test_transform.py | 12 ++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/writerai/_utils/_transform.py b/src/writerai/_utils/_transform.py index 7ac2e17f..3ec62081 100644 --- a/src/writerai/_utils/_transform.py +++ b/src/writerai/_utils/_transform.py @@ -142,6 +142,10 @@ def _maybe_transform_key(key: str, type_: type) -> str: return key +def _no_transform_needed(annotation: type) -> bool: + return annotation == float or annotation == int + + def _transform_recursive( data: object, *, @@ -184,6 +188,15 @@ def _transform_recursive( return cast(object, data) inner_type = extract_type_arg(stripped_type, 0) + if _no_transform_needed(inner_type): + # for some types there is no need to transform anything, so we can get a small + # perf boost from skipping that work. + # + # but we still need to convert to a list to ensure the data is json-serializable + if is_list(data): + return data + return list(data) + return [_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data] if is_union_type(stripped_type): @@ -332,6 +345,15 @@ async def _async_transform_recursive( return cast(object, data) inner_type = extract_type_arg(stripped_type, 0) + if _no_transform_needed(inner_type): + # for some types there is no need to transform anything, so we can get a small + # perf boost from skipping that work. + # + # but we still need to convert to a list to ensure the data is json-serializable + if is_list(data): + return data + return list(data) + return [await _async_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data] if is_union_type(stripped_type): diff --git a/tests/test_transform.py b/tests/test_transform.py index 07bc24e8..6db8111d 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -432,3 +432,15 @@ async def test_base64_file_input(use_async: bool) -> None: assert await transform({"foo": io.BytesIO(b"Hello, world!")}, TypedDictBase64Input, use_async) == { "foo": "SGVsbG8sIHdvcmxkIQ==" } # type: ignore[comparison-overlap] + + +@parametrize +@pytest.mark.asyncio +async def test_transform_skipping(use_async: bool) -> None: + # lists of ints are left as-is + data = [1, 2, 3] + assert await transform(data, List[int], use_async) is data + + # iterables of ints are converted to a list + data = iter([1, 2, 3]) + assert await transform(data, Iterable[int], use_async) == [1, 2, 3] From 4eb280c34d96b74519f38d9df660c663f273324d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 9 Apr 2025 16:30:46 +0000 Subject: [PATCH 222/399] chore(internal): expand CI branch coverage (#235) --- .github/workflows/ci.yml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b286e5a..53a3a09c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,18 +1,18 @@ name: CI on: push: - branches: - - main - pull_request: - branches: - - main - - next + branches-ignore: + - 'generated' + - 'codegen/**' + - 'integrated/**' + - 'preview-head/**' + - 'preview-base/**' + - 'preview/**' jobs: lint: name: lint runs-on: ubuntu-latest - steps: - uses: actions/checkout@v4 @@ -33,7 +33,6 @@ jobs: test: name: test runs-on: ubuntu-latest - steps: - uses: actions/checkout@v4 From 052ec5255e38627e6f38a5fd3ec979be9f03f434 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 9 Apr 2025 21:06:50 +0000 Subject: [PATCH 223/399] chore(internal): reduce CI branch coverage --- .github/workflows/ci.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 53a3a09c..81f6dc20 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,13 +1,12 @@ name: CI on: push: - branches-ignore: - - 'generated' - - 'codegen/**' - - 'integrated/**' - - 'preview-head/**' - - 'preview-base/**' - - 'preview/**' + branches: + - main + pull_request: + branches: + - main + - next jobs: lint: From 1ccdd395cecdde84f5d20a26c752528d479372e4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 11 Apr 2025 12:22:02 +0000 Subject: [PATCH 224/399] fix(perf): skip traversing types for NotGiven values --- src/writerai/_utils/_transform.py | 11 +++++++++++ tests/test_transform.py | 9 ++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/writerai/_utils/_transform.py b/src/writerai/_utils/_transform.py index 3ec62081..3b2b8e00 100644 --- a/src/writerai/_utils/_transform.py +++ b/src/writerai/_utils/_transform.py @@ -12,6 +12,7 @@ from ._utils import ( is_list, + is_given, is_mapping, is_iterable, ) @@ -258,6 +259,11 @@ def _transform_typeddict( result: dict[str, object] = {} annotations = get_type_hints(expected_type, include_extras=True) for key, value in data.items(): + if not is_given(value): + # we don't need to include `NotGiven` values here as they'll + # be stripped out before the request is sent anyway + continue + type_ = annotations.get(key) if type_ is None: # we do not have a type annotation for this field, leave it as is @@ -415,6 +421,11 @@ async def _async_transform_typeddict( result: dict[str, object] = {} annotations = get_type_hints(expected_type, include_extras=True) for key, value in data.items(): + if not is_given(value): + # we don't need to include `NotGiven` values here as they'll + # be stripped out before the request is sent anyway + continue + type_ = annotations.get(key) if type_ is None: # we do not have a type annotation for this field, leave it as is diff --git a/tests/test_transform.py b/tests/test_transform.py index 6db8111d..69233f26 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -8,7 +8,7 @@ import pytest -from writerai._types import Base64FileInput +from writerai._types import NOT_GIVEN, Base64FileInput from writerai._utils import ( PropertyInfo, transform as _transform, @@ -444,3 +444,10 @@ async def test_transform_skipping(use_async: bool) -> None: # iterables of ints are converted to a list data = iter([1, 2, 3]) assert await transform(data, Iterable[int], use_async) == [1, 2, 3] + + +@parametrize +@pytest.mark.asyncio +async def test_strips_notgiven(use_async: bool) -> None: + assert await transform({"foo_bar": "bar"}, Foo1, use_async) == {"fooBar": "bar"} + assert await transform({"foo_bar": NOT_GIVEN}, Foo1, use_async) == {} From 1d6801a844bb8e3e14ca8a5529a28276a55e2f76 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 11 Apr 2025 13:33:19 +0000 Subject: [PATCH 225/399] fix(perf): optimize some hot paths --- src/writerai/_utils/_transform.py | 14 +++++++++++++- src/writerai/_utils/_typing.py | 2 ++ .../resources/applications/applications.py | 8 ++++++-- src/writerai/resources/chat.py | 4 ++-- src/writerai/resources/completions.py | 8 ++++++-- src/writerai/resources/graphs.py | 8 ++++++-- 6 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/writerai/_utils/_transform.py b/src/writerai/_utils/_transform.py index 3b2b8e00..b0cc20a7 100644 --- a/src/writerai/_utils/_transform.py +++ b/src/writerai/_utils/_transform.py @@ -5,7 +5,7 @@ import pathlib from typing import Any, Mapping, TypeVar, cast from datetime import date, datetime -from typing_extensions import Literal, get_args, override, get_type_hints +from typing_extensions import Literal, get_args, override, get_type_hints as _get_type_hints import anyio import pydantic @@ -13,6 +13,7 @@ from ._utils import ( is_list, is_given, + lru_cache, is_mapping, is_iterable, ) @@ -109,6 +110,7 @@ class Params(TypedDict, total=False): return cast(_T, transformed) +@lru_cache(maxsize=8096) def _get_annotated_type(type_: type) -> type | None: """If the given type is an `Annotated` type then it is returned, if not `None` is returned. @@ -433,3 +435,13 @@ async def _async_transform_typeddict( else: result[_maybe_transform_key(key, type_)] = await _async_transform_recursive(value, annotation=type_) return result + + +@lru_cache(maxsize=8096) +def get_type_hints( + obj: Any, + globalns: dict[str, Any] | None = None, + localns: Mapping[str, Any] | None = None, + include_extras: bool = False, +) -> dict[str, Any]: + return _get_type_hints(obj, globalns=globalns, localns=localns, include_extras=include_extras) diff --git a/src/writerai/_utils/_typing.py b/src/writerai/_utils/_typing.py index 278749b1..1958820f 100644 --- a/src/writerai/_utils/_typing.py +++ b/src/writerai/_utils/_typing.py @@ -13,6 +13,7 @@ get_origin, ) +from ._utils import lru_cache from .._types import InheritsGeneric from .._compat import is_union as _is_union @@ -66,6 +67,7 @@ def is_type_alias_type(tp: Any, /) -> TypeIs[typing_extensions.TypeAliasType]: # Extracts T from Annotated[T, ...] or from Required[Annotated[T, ...]] +@lru_cache(maxsize=8096) def strip_annotated_type(typ: type) -> type: if is_required_type(typ) or is_annotated_type(typ): return strip_annotated_type(cast(type, get_args(typ)[0])) diff --git a/src/writerai/resources/applications/applications.py b/src/writerai/resources/applications/applications.py index 06a8028a..20df4644 100644 --- a/src/writerai/resources/applications/applications.py +++ b/src/writerai/resources/applications/applications.py @@ -287,7 +287,9 @@ def generate_content( "inputs": inputs, "stream": stream, }, - application_generate_content_params.ApplicationGenerateContentParams, + application_generate_content_params.ApplicationGenerateContentParamsStreaming + if stream + else application_generate_content_params.ApplicationGenerateContentParamsNonStreaming, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -536,7 +538,9 @@ async def generate_content( "inputs": inputs, "stream": stream, }, - application_generate_content_params.ApplicationGenerateContentParams, + application_generate_content_params.ApplicationGenerateContentParamsStreaming + if stream + else application_generate_content_params.ApplicationGenerateContentParamsNonStreaming, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout diff --git a/src/writerai/resources/chat.py b/src/writerai/resources/chat.py index cd7ac385..9a092569 100644 --- a/src/writerai/resources/chat.py +++ b/src/writerai/resources/chat.py @@ -346,7 +346,7 @@ def chat( "tools": tools, "top_p": top_p, }, - chat_chat_params.ChatChatParams, + chat_chat_params.ChatChatParamsStreaming if stream else chat_chat_params.ChatChatParamsNonStreaming, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -672,7 +672,7 @@ async def chat( "tools": tools, "top_p": top_p, }, - chat_chat_params.ChatChatParams, + chat_chat_params.ChatChatParamsStreaming if stream else chat_chat_params.ChatChatParamsNonStreaming, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout diff --git a/src/writerai/resources/completions.py b/src/writerai/resources/completions.py index dc8b8251..3b1cd35d 100644 --- a/src/writerai/resources/completions.py +++ b/src/writerai/resources/completions.py @@ -267,7 +267,9 @@ def create( "temperature": temperature, "top_p": top_p, }, - completion_create_params.CompletionCreateParams, + completion_create_params.CompletionCreateParamsStreaming + if stream + else completion_create_params.CompletionCreateParamsNonStreaming, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -515,7 +517,9 @@ async def create( "temperature": temperature, "top_p": top_p, }, - completion_create_params.CompletionCreateParams, + completion_create_params.CompletionCreateParamsStreaming + if stream + else completion_create_params.CompletionCreateParamsNonStreaming, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout diff --git a/src/writerai/resources/graphs.py b/src/writerai/resources/graphs.py index a3a044f5..6e369c7f 100644 --- a/src/writerai/resources/graphs.py +++ b/src/writerai/resources/graphs.py @@ -459,7 +459,9 @@ def question( "stream": stream, "subqueries": subqueries, }, - graph_question_params.GraphQuestionParams, + graph_question_params.GraphQuestionParamsStreaming + if stream + else graph_question_params.GraphQuestionParamsNonStreaming, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -924,7 +926,9 @@ async def question( "stream": stream, "subqueries": subqueries, }, - graph_question_params.GraphQuestionParams, + graph_question_params.GraphQuestionParamsStreaming + if stream + else graph_question_params.GraphQuestionParamsNonStreaming, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout From 6d6d642bdf92302aa348685574dd73f13b1cfaa5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 14 Apr 2025 12:24:46 +0000 Subject: [PATCH 226/399] chore(internal): update pyright settings --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 78fbdac7..64fb6092 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -147,6 +147,7 @@ exclude = [ ] reportImplicitOverride = true +reportOverlappingOverload = false reportImportCycles = false reportPrivateUsage = false From 8978c87bae87f647f7e8547ba483b277d1664b5e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 14 Apr 2025 14:33:32 +0000 Subject: [PATCH 227/399] chore(client): minor internal fixes --- src/writerai/_base_client.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index 36bb1271..16254249 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -409,7 +409,8 @@ def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0 idempotency_header = self._idempotency_header if idempotency_header and options.method.lower() != "get" and idempotency_header not in headers: - headers[idempotency_header] = options.idempotency_key or self._idempotency_key() + options.idempotency_key = options.idempotency_key or self._idempotency_key() + headers[idempotency_header] = options.idempotency_key # Don't set these headers if they were already set or removed by the caller. We check # `custom_headers`, which can contain `Omit()`, instead of `headers` to account for the removal case. @@ -943,6 +944,10 @@ def _request( request = self._build_request(options, retries_taken=retries_taken) self._prepare_request(request) + if options.idempotency_key: + # ensure the idempotency key is reused between requests + input_options.idempotency_key = options.idempotency_key + kwargs: HttpxSendArgs = {} if self.custom_auth is not None: kwargs["auth"] = self.custom_auth @@ -1475,6 +1480,10 @@ async def _request( request = self._build_request(options, retries_taken=retries_taken) await self._prepare_request(request) + if options.idempotency_key: + # ensure the idempotency key is reused between requests + input_options.idempotency_key = options.idempotency_key + kwargs: HttpxSendArgs = {} if self.custom_auth is not None: kwargs["auth"] = self.custom_auth From 0521640e36378d6680e87182464dfab2172ded69 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 16 Apr 2025 12:07:04 +0000 Subject: [PATCH 228/399] chore(internal): bump pyright version --- pyproject.toml | 2 +- requirements-dev.lock | 2 +- src/writerai/_base_client.py | 6 +++++- src/writerai/_models.py | 1 - src/writerai/_utils/_typing.py | 2 +- tests/conftest.py | 2 +- tests/test_models.py | 2 +- 7 files changed, 10 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 64fb6092..36673967 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ Repository = "https://github.com/writer/writer-python" managed = true # version pins are in requirements-dev.lock dev-dependencies = [ - "pyright>=1.1.359", + "pyright==1.1.399", "mypy", "respx", "pytest", diff --git a/requirements-dev.lock b/requirements-dev.lock index 857dd051..56d7ab37 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -69,7 +69,7 @@ pydantic-core==2.27.1 # via pydantic pygments==2.18.0 # via rich -pyright==1.1.392.post0 +pyright==1.1.399 pytest==8.3.3 # via pytest-asyncio pytest-asyncio==0.24.0 diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index 16254249..86e7f4d9 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -98,7 +98,11 @@ _AsyncStreamT = TypeVar("_AsyncStreamT", bound=AsyncStream[Any]) if TYPE_CHECKING: - from httpx._config import DEFAULT_TIMEOUT_CONFIG as HTTPX_DEFAULT_TIMEOUT + from httpx._config import ( + DEFAULT_TIMEOUT_CONFIG, # pyright: ignore[reportPrivateImportUsage] + ) + + HTTPX_DEFAULT_TIMEOUT = DEFAULT_TIMEOUT_CONFIG else: try: from httpx._config import DEFAULT_TIMEOUT_CONFIG as HTTPX_DEFAULT_TIMEOUT diff --git a/src/writerai/_models.py b/src/writerai/_models.py index 34935716..58b9263e 100644 --- a/src/writerai/_models.py +++ b/src/writerai/_models.py @@ -19,7 +19,6 @@ ) import pydantic -import pydantic.generics from pydantic.fields import FieldInfo from ._types import ( diff --git a/src/writerai/_utils/_typing.py b/src/writerai/_utils/_typing.py index 1958820f..1bac9542 100644 --- a/src/writerai/_utils/_typing.py +++ b/src/writerai/_utils/_typing.py @@ -110,7 +110,7 @@ class MyResponse(Foo[_T]): ``` """ cls = cast(object, get_origin(typ) or typ) - if cls in generic_bases: + if cls in generic_bases: # pyright: ignore[reportUnnecessaryContains] # we're given the class directly return extract_type_arg(typ, index) diff --git a/tests/conftest.py b/tests/conftest.py index 573ff773..5ba6ea6a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,7 @@ from writerai import Writer, AsyncWriter if TYPE_CHECKING: - from _pytest.fixtures import FixtureRequest + from _pytest.fixtures import FixtureRequest # pyright: ignore[reportPrivateImportUsage] pytest.register_assert_rewrite("tests.utils") diff --git a/tests/test_models.py b/tests/test_models.py index 91faed5a..b26b2ad3 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -832,7 +832,7 @@ class B(BaseModel): @pytest.mark.skipif(not PYDANTIC_V2, reason="TypeAliasType is not supported in Pydantic v1") def test_type_alias_type() -> None: - Alias = TypeAliasType("Alias", str) + Alias = TypeAliasType("Alias", str) # pyright: ignore class Model(BaseModel): alias: Alias From 023a1625d2b5e2c8737e0878a2efda02f306f6b1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 16 Apr 2025 13:01:10 +0000 Subject: [PATCH 229/399] chore(internal): base client updates --- src/writerai/_base_client.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index 86e7f4d9..c055609b 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -119,6 +119,7 @@ class PageInfo: url: URL | NotGiven params: Query | NotGiven + json: Body | NotGiven @overload def __init__( @@ -134,19 +135,30 @@ def __init__( params: Query, ) -> None: ... + @overload + def __init__( + self, + *, + json: Body, + ) -> None: ... + def __init__( self, *, url: URL | NotGiven = NOT_GIVEN, + json: Body | NotGiven = NOT_GIVEN, params: Query | NotGiven = NOT_GIVEN, ) -> None: self.url = url + self.json = json self.params = params @override def __repr__(self) -> str: if self.url: return f"{self.__class__.__name__}(url={self.url})" + if self.json: + return f"{self.__class__.__name__}(json={self.json})" return f"{self.__class__.__name__}(params={self.params})" @@ -195,6 +207,19 @@ def _info_to_options(self, info: PageInfo) -> FinalRequestOptions: options.url = str(url) return options + if not isinstance(info.json, NotGiven): + if not is_mapping(info.json): + raise TypeError("Pagination is only supported with mappings") + + if not options.json_data: + options.json_data = {**info.json} + else: + if not is_mapping(options.json_data): + raise TypeError("Pagination is only supported with mappings") + + options.json_data = {**options.json_data, **info.json} + return options + raise ValueError("Unexpected PageInfo state") From 260748a64d4864bf27d2a3c03bdc34777d9bcd78 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 18 Apr 2025 10:18:56 +0000 Subject: [PATCH 230/399] chore(internal): update models test --- tests/test_models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_models.py b/tests/test_models.py index b26b2ad3..93d48eea 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -492,12 +492,15 @@ class Model(BaseModel): resource_id: Optional[str] = None m = Model.construct() + assert m.resource_id is None assert "resource_id" not in m.model_fields_set m = Model.construct(resource_id=None) + assert m.resource_id is None assert "resource_id" in m.model_fields_set m = Model.construct(resource_id="foo") + assert m.resource_id == "foo" assert "resource_id" in m.model_fields_set From 205c82bdb1f1ddd293da2fd73166965bffb8dfee Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 21 Apr 2025 20:21:46 +0000 Subject: [PATCH 231/399] docs(api): updates to API spec --- .stats.yml | 4 +- README.md | 2 +- .../resources/applications/applications.py | 34 ++++--- src/writerai/resources/applications/graphs.py | 12 +-- src/writerai/resources/applications/jobs.py | 6 +- src/writerai/resources/chat.py | 88 +++++++++++-------- src/writerai/resources/completions.py | 34 ++++--- src/writerai/resources/files.py | 4 +- src/writerai/resources/graphs.py | 24 ++--- src/writerai/resources/tools/comprehend.py | 16 ++-- src/writerai/resources/tools/tools.py | 8 +- src/writerai/resources/vision.py | 11 ++- .../application_generate_content_params.py | 8 +- .../types/applications/job_create_params.py | 8 +- src/writerai/types/chat_chat_params.py | 21 +++-- .../types/completion_create_params.py | 9 +- src/writerai/types/file_retry_params.py | 2 +- src/writerai/types/graph_question_params.py | 4 +- src/writerai/types/model_list_response.py | 5 +- src/writerai/types/shared/tool_param.py | 12 +-- .../types/shared_params/tool_param.py | 12 +-- .../tool_context_aware_splitting_params.py | 4 +- .../types/tools/comprehend_medical_params.py | 4 +- src/writerai/types/vision_analyze_params.py | 11 +-- tests/api_resources/test_chat.py | 32 +++---- tests/test_client.py | 12 +-- 26 files changed, 211 insertions(+), 176 deletions(-) diff --git a/.stats.yml b/.stats.yml index 12896beb..487e77f6 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-d15316b8a3a086ae9ec8eea0d436b0885262df9bcf23b9587ad059e50357c220.yml -openapi_spec_hash: 4f81a4f4840438f80eff345e76ead962 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-a8e20e751972f7b8c948a833764b02d0835b380189eaf7393d63eb2250dbd598.yml +openapi_spec_hash: 6f27335468bc5bf01fbcd1454330fb07 config_hash: ee2e5a914fd298a6f4c740f5ce187f87 diff --git a/README.md b/README.md index f3d1b275..d44a0163 100644 --- a/README.md +++ b/README.md @@ -207,7 +207,7 @@ client = Writer() chat_completion = client.chat.chat( messages=[{"role": "user"}], - model="model", + model="palmyra-x-004", stream_options={"include_usage": True}, ) print(chat_completion.stream_options) diff --git a/src/writerai/resources/applications/applications.py b/src/writerai/resources/applications/applications.py index 20df4644..b8f5010d 100644 --- a/src/writerai/resources/applications/applications.py +++ b/src/writerai/resources/applications/applications.py @@ -89,8 +89,8 @@ def retrieve( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ApplicationRetrieveResponse: """ - Retrieves detailed information for a specific no-code application, including its - configuration and current status. + Retrieves detailed information for a specific no-code agent (formerly called + no-code applications), including its configuration and current status. Args: extra_headers: Send extra headers @@ -127,8 +127,8 @@ def list( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> SyncCursorPage[ApplicationListResponse]: """ - Retrieves a paginated list of no-code applications with optional filtering and - sorting capabilities. + Retrieves a paginated list of no-code agents (formerly called no-code + applications) with optional filtering and sorting capabilities. Args: after: Return results after this application ID for pagination. @@ -186,7 +186,8 @@ def generate_content( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ApplicationGenerateContentResponse: """ - Generate content from an existing no-code application with inputs. + Generate content from an existing no-code agent (formerly called no-code + applications) with inputs. Args: stream: Indicates whether the response should be streamed. Currently only supported for @@ -217,7 +218,8 @@ def generate_content( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> Stream[ApplicationGenerateContentChunk]: """ - Generate content from an existing no-code application with inputs. + Generate content from an existing no-code agent (formerly called no-code + applications) with inputs. Args: stream: Indicates whether the response should be streamed. Currently only supported for @@ -248,7 +250,8 @@ def generate_content( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ApplicationGenerateContentResponse | Stream[ApplicationGenerateContentChunk]: """ - Generate content from an existing no-code application with inputs. + Generate content from an existing no-code agent (formerly called no-code + applications) with inputs. Args: stream: Indicates whether the response should be streamed. Currently only supported for @@ -340,8 +343,8 @@ async def retrieve( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ApplicationRetrieveResponse: """ - Retrieves detailed information for a specific no-code application, including its - configuration and current status. + Retrieves detailed information for a specific no-code agent (formerly called + no-code applications), including its configuration and current status. Args: extra_headers: Send extra headers @@ -378,8 +381,8 @@ def list( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> AsyncPaginator[ApplicationListResponse, AsyncCursorPage[ApplicationListResponse]]: """ - Retrieves a paginated list of no-code applications with optional filtering and - sorting capabilities. + Retrieves a paginated list of no-code agents (formerly called no-code + applications) with optional filtering and sorting capabilities. Args: after: Return results after this application ID for pagination. @@ -437,7 +440,8 @@ async def generate_content( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ApplicationGenerateContentResponse: """ - Generate content from an existing no-code application with inputs. + Generate content from an existing no-code agent (formerly called no-code + applications) with inputs. Args: stream: Indicates whether the response should be streamed. Currently only supported for @@ -468,7 +472,8 @@ async def generate_content( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> AsyncStream[ApplicationGenerateContentChunk]: """ - Generate content from an existing no-code application with inputs. + Generate content from an existing no-code agent (formerly called no-code + applications) with inputs. Args: stream: Indicates whether the response should be streamed. Currently only supported for @@ -499,7 +504,8 @@ async def generate_content( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ApplicationGenerateContentResponse | AsyncStream[ApplicationGenerateContentChunk]: """ - Generate content from an existing no-code application with inputs. + Generate content from an existing no-code agent (formerly called no-code + applications) with inputs. Args: stream: Indicates whether the response should be streamed. Currently only supported for diff --git a/src/writerai/resources/applications/graphs.py b/src/writerai/resources/applications/graphs.py index 5e9a7fc6..0444346f 100644 --- a/src/writerai/resources/applications/graphs.py +++ b/src/writerai/resources/applications/graphs.py @@ -59,8 +59,7 @@ def update( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ApplicationGraphsResponse: """ - Updates the Knowledge Graphs listed and associates them with the no-code chat - app to be used. + Updates the Knowledge Graphs listed and associates them with the no-code agent. Args: graph_ids: A list of Knowledge Graph IDs to associate with the application. Note that this @@ -98,7 +97,8 @@ def list( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ApplicationGraphsResponse: """ - Retrieve Knowledge Graphs associated with a no-code chat application. + Retrieve Knowledge Graphs associated with a no-code agent that has chat + capabilities. Args: extra_headers: Send extra headers @@ -153,8 +153,7 @@ async def update( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ApplicationGraphsResponse: """ - Updates the Knowledge Graphs listed and associates them with the no-code chat - app to be used. + Updates the Knowledge Graphs listed and associates them with the no-code agent. Args: graph_ids: A list of Knowledge Graph IDs to associate with the application. Note that this @@ -192,7 +191,8 @@ async def list( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ApplicationGraphsResponse: """ - Retrieve Knowledge Graphs associated with a no-code chat application. + Retrieve Knowledge Graphs associated with a no-code agent that has chat + capabilities. Args: extra_headers: Send extra headers diff --git a/src/writerai/resources/applications/jobs.py b/src/writerai/resources/applications/jobs.py index 779402c6..b66e40d0 100644 --- a/src/writerai/resources/applications/jobs.py +++ b/src/writerai/resources/applications/jobs.py @@ -63,7 +63,8 @@ def create( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> JobCreateResponse: """ - Generate content asynchronously from an existing application with inputs. + Generate content asynchronously from an existing no-code agent (formerly called + no-code applications) with inputs. Args: inputs: A list of input objects to generate content for. @@ -243,7 +244,8 @@ async def create( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> JobCreateResponse: """ - Generate content asynchronously from an existing application with inputs. + Generate content asynchronously from an existing no-code agent (formerly called + no-code applications) with inputs. Args: inputs: A list of input objects to generate content for. diff --git a/src/writerai/resources/chat.py b/src/writerai/resources/chat.py index 9a092569..8d264f2f 100644 --- a/src/writerai/resources/chat.py +++ b/src/writerai/resources/chat.py @@ -56,7 +56,7 @@ def chat( self, *, messages: Iterable[chat_chat_params.Message], - model: str, + model: Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"], logprobs: bool | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, @@ -78,14 +78,14 @@ def chat( The response shown below is for non-streaming. To learn about streaming responses, see the - [chat completion guide](/api-guides/chat-completion). + [chat completion guide](https://dev.writer.com/api-guides/chat-completion). Args: messages: An array of message objects that form the conversation history or context for the model to respond to. The array must contain at least one message. - model: Specifies the model to be used for generating responses. The chat model is - always `palmyra-x-004` for conversational use. + model: The [ID of the model](https://dev.writer.com/home/models) to use for creating + the chat completion. logprobs: Specifies whether to return log probabilities of the output tokens. @@ -94,7 +94,7 @@ def chat( to allow for longer or shorter responses as needed. n: Specifies the number of completions (responses) to generate from the model in a - single request. This parameter allows multiple responses to be generated, + single request. This parameter allows for generating multiple responses, offering a variety of potential replies from which to choose. stop: A token or sequence of tokens that, when generated, will cause the model to stop @@ -119,7 +119,9 @@ def chat( generate responses. The tool definitions use JSON schema. You can define your own functions or use one of the built-in `graph`, `llm`, or `vision` tools. Note that you can only use one built-in tool type in the array (only one of `graph`, - `llm`, or `vision`). + `llm`, or `vision`). You can pass multiple custom + tools](https://dev.writer.com/api-guides/tool-calling) of type `function` in the + same request. top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with @@ -141,7 +143,7 @@ def chat( self, *, messages: Iterable[chat_chat_params.Message], - model: str, + model: Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"], stream: Literal[True], logprobs: bool | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, @@ -163,14 +165,14 @@ def chat( The response shown below is for non-streaming. To learn about streaming responses, see the - [chat completion guide](/api-guides/chat-completion). + [chat completion guide](https://dev.writer.com/api-guides/chat-completion). Args: messages: An array of message objects that form the conversation history or context for the model to respond to. The array must contain at least one message. - model: Specifies the model to be used for generating responses. The chat model is - always `palmyra-x-004` for conversational use. + model: The [ID of the model](https://dev.writer.com/home/models) to use for creating + the chat completion. stream: Indicates whether the response should be streamed incrementally as it is generated or only returned once fully complete. Streaming can be useful for @@ -183,7 +185,7 @@ def chat( to allow for longer or shorter responses as needed. n: Specifies the number of completions (responses) to generate from the model in a - single request. This parameter allows multiple responses to be generated, + single request. This parameter allows for generating multiple responses, offering a variety of potential replies from which to choose. stop: A token or sequence of tokens that, when generated, will cause the model to stop @@ -204,7 +206,9 @@ def chat( generate responses. The tool definitions use JSON schema. You can define your own functions or use one of the built-in `graph`, `llm`, or `vision` tools. Note that you can only use one built-in tool type in the array (only one of `graph`, - `llm`, or `vision`). + `llm`, or `vision`). You can pass multiple custom + tools](https://dev.writer.com/api-guides/tool-calling) of type `function` in the + same request. top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with @@ -226,7 +230,7 @@ def chat( self, *, messages: Iterable[chat_chat_params.Message], - model: str, + model: Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"], stream: bool, logprobs: bool | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, @@ -248,14 +252,14 @@ def chat( The response shown below is for non-streaming. To learn about streaming responses, see the - [chat completion guide](/api-guides/chat-completion). + [chat completion guide](https://dev.writer.com/api-guides/chat-completion). Args: messages: An array of message objects that form the conversation history or context for the model to respond to. The array must contain at least one message. - model: Specifies the model to be used for generating responses. The chat model is - always `palmyra-x-004` for conversational use. + model: The [ID of the model](https://dev.writer.com/home/models) to use for creating + the chat completion. stream: Indicates whether the response should be streamed incrementally as it is generated or only returned once fully complete. Streaming can be useful for @@ -268,7 +272,7 @@ def chat( to allow for longer or shorter responses as needed. n: Specifies the number of completions (responses) to generate from the model in a - single request. This parameter allows multiple responses to be generated, + single request. This parameter allows for generating multiple responses, offering a variety of potential replies from which to choose. stop: A token or sequence of tokens that, when generated, will cause the model to stop @@ -289,7 +293,9 @@ def chat( generate responses. The tool definitions use JSON schema. You can define your own functions or use one of the built-in `graph`, `llm`, or `vision` tools. Note that you can only use one built-in tool type in the array (only one of `graph`, - `llm`, or `vision`). + `llm`, or `vision`). You can pass multiple custom + tools](https://dev.writer.com/api-guides/tool-calling) of type `function` in the + same request. top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with @@ -311,7 +317,7 @@ def chat( self, *, messages: Iterable[chat_chat_params.Message], - model: str, + model: Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"], logprobs: bool | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, @@ -382,7 +388,7 @@ async def chat( self, *, messages: Iterable[chat_chat_params.Message], - model: str, + model: Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"], logprobs: bool | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, @@ -404,14 +410,14 @@ async def chat( The response shown below is for non-streaming. To learn about streaming responses, see the - [chat completion guide](/api-guides/chat-completion). + [chat completion guide](https://dev.writer.com/api-guides/chat-completion). Args: messages: An array of message objects that form the conversation history or context for the model to respond to. The array must contain at least one message. - model: Specifies the model to be used for generating responses. The chat model is - always `palmyra-x-004` for conversational use. + model: The [ID of the model](https://dev.writer.com/home/models) to use for creating + the chat completion. logprobs: Specifies whether to return log probabilities of the output tokens. @@ -420,7 +426,7 @@ async def chat( to allow for longer or shorter responses as needed. n: Specifies the number of completions (responses) to generate from the model in a - single request. This parameter allows multiple responses to be generated, + single request. This parameter allows for generating multiple responses, offering a variety of potential replies from which to choose. stop: A token or sequence of tokens that, when generated, will cause the model to stop @@ -445,7 +451,9 @@ async def chat( generate responses. The tool definitions use JSON schema. You can define your own functions or use one of the built-in `graph`, `llm`, or `vision` tools. Note that you can only use one built-in tool type in the array (only one of `graph`, - `llm`, or `vision`). + `llm`, or `vision`). You can pass multiple custom + tools](https://dev.writer.com/api-guides/tool-calling) of type `function` in the + same request. top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with @@ -467,7 +475,7 @@ async def chat( self, *, messages: Iterable[chat_chat_params.Message], - model: str, + model: Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"], stream: Literal[True], logprobs: bool | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, @@ -489,14 +497,14 @@ async def chat( The response shown below is for non-streaming. To learn about streaming responses, see the - [chat completion guide](/api-guides/chat-completion). + [chat completion guide](https://dev.writer.com/api-guides/chat-completion). Args: messages: An array of message objects that form the conversation history or context for the model to respond to. The array must contain at least one message. - model: Specifies the model to be used for generating responses. The chat model is - always `palmyra-x-004` for conversational use. + model: The [ID of the model](https://dev.writer.com/home/models) to use for creating + the chat completion. stream: Indicates whether the response should be streamed incrementally as it is generated or only returned once fully complete. Streaming can be useful for @@ -509,7 +517,7 @@ async def chat( to allow for longer or shorter responses as needed. n: Specifies the number of completions (responses) to generate from the model in a - single request. This parameter allows multiple responses to be generated, + single request. This parameter allows for generating multiple responses, offering a variety of potential replies from which to choose. stop: A token or sequence of tokens that, when generated, will cause the model to stop @@ -530,7 +538,9 @@ async def chat( generate responses. The tool definitions use JSON schema. You can define your own functions or use one of the built-in `graph`, `llm`, or `vision` tools. Note that you can only use one built-in tool type in the array (only one of `graph`, - `llm`, or `vision`). + `llm`, or `vision`). You can pass multiple custom + tools](https://dev.writer.com/api-guides/tool-calling) of type `function` in the + same request. top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with @@ -552,7 +562,7 @@ async def chat( self, *, messages: Iterable[chat_chat_params.Message], - model: str, + model: Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"], stream: bool, logprobs: bool | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, @@ -574,14 +584,14 @@ async def chat( The response shown below is for non-streaming. To learn about streaming responses, see the - [chat completion guide](/api-guides/chat-completion). + [chat completion guide](https://dev.writer.com/api-guides/chat-completion). Args: messages: An array of message objects that form the conversation history or context for the model to respond to. The array must contain at least one message. - model: Specifies the model to be used for generating responses. The chat model is - always `palmyra-x-004` for conversational use. + model: The [ID of the model](https://dev.writer.com/home/models) to use for creating + the chat completion. stream: Indicates whether the response should be streamed incrementally as it is generated or only returned once fully complete. Streaming can be useful for @@ -594,7 +604,7 @@ async def chat( to allow for longer or shorter responses as needed. n: Specifies the number of completions (responses) to generate from the model in a - single request. This parameter allows multiple responses to be generated, + single request. This parameter allows for generating multiple responses, offering a variety of potential replies from which to choose. stop: A token or sequence of tokens that, when generated, will cause the model to stop @@ -615,7 +625,9 @@ async def chat( generate responses. The tool definitions use JSON schema. You can define your own functions or use one of the built-in `graph`, `llm`, or `vision` tools. Note that you can only use one built-in tool type in the array (only one of `graph`, - `llm`, or `vision`). + `llm`, or `vision`). You can pass multiple custom + tools](https://dev.writer.com/api-guides/tool-calling) of type `function` in the + same request. top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with @@ -637,7 +649,7 @@ async def chat( self, *, messages: Iterable[chat_chat_params.Message], - model: str, + model: Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"], logprobs: bool | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, diff --git a/src/writerai/resources/completions.py b/src/writerai/resources/completions.py index 3b1cd35d..6e9357cf 100644 --- a/src/writerai/resources/completions.py +++ b/src/writerai/resources/completions.py @@ -54,7 +54,7 @@ def with_streaming_response(self) -> CompletionsResourceWithStreamingResponse: def create( self, *, - model: str, + model: Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"], prompt: str, best_of: int | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, @@ -74,7 +74,8 @@ def create( Text generation Args: - model: The identifier of the model to be used for processing the request. + model: The [ID of the model](https://dev.writer.com/home/models) to use for generating + text. prompt: The input text that the model will process to generate a response. @@ -115,7 +116,7 @@ def create( def create( self, *, - model: str, + model: Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"], prompt: str, stream: Literal[True], best_of: int | NotGiven = NOT_GIVEN, @@ -135,7 +136,8 @@ def create( Text generation Args: - model: The identifier of the model to be used for processing the request. + model: The [ID of the model](https://dev.writer.com/home/models) to use for generating + text. prompt: The input text that the model will process to generate a response. @@ -176,7 +178,7 @@ def create( def create( self, *, - model: str, + model: Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"], prompt: str, stream: bool, best_of: int | NotGiven = NOT_GIVEN, @@ -196,7 +198,8 @@ def create( Text generation Args: - model: The identifier of the model to be used for processing the request. + model: The [ID of the model](https://dev.writer.com/home/models) to use for generating + text. prompt: The input text that the model will process to generate a response. @@ -237,7 +240,7 @@ def create( def create( self, *, - model: str, + model: Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"], prompt: str, best_of: int | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, @@ -304,7 +307,7 @@ def with_streaming_response(self) -> AsyncCompletionsResourceWithStreamingRespon async def create( self, *, - model: str, + model: Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"], prompt: str, best_of: int | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, @@ -324,7 +327,8 @@ async def create( Text generation Args: - model: The identifier of the model to be used for processing the request. + model: The [ID of the model](https://dev.writer.com/home/models) to use for generating + text. prompt: The input text that the model will process to generate a response. @@ -365,7 +369,7 @@ async def create( async def create( self, *, - model: str, + model: Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"], prompt: str, stream: Literal[True], best_of: int | NotGiven = NOT_GIVEN, @@ -385,7 +389,8 @@ async def create( Text generation Args: - model: The identifier of the model to be used for processing the request. + model: The [ID of the model](https://dev.writer.com/home/models) to use for generating + text. prompt: The input text that the model will process to generate a response. @@ -426,7 +431,7 @@ async def create( async def create( self, *, - model: str, + model: Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"], prompt: str, stream: bool, best_of: int | NotGiven = NOT_GIVEN, @@ -446,7 +451,8 @@ async def create( Text generation Args: - model: The identifier of the model to be used for processing the request. + model: The [ID of the model](https://dev.writer.com/home/models) to use for generating + text. prompt: The input text that the model will process to generate a response. @@ -487,7 +493,7 @@ async def create( async def create( self, *, - model: str, + model: Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"], prompt: str, best_of: int | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, diff --git a/src/writerai/resources/files.py b/src/writerai/resources/files.py index aec5a5ec..2f530cd7 100644 --- a/src/writerai/resources/files.py +++ b/src/writerai/resources/files.py @@ -242,7 +242,7 @@ def retry( Retry failed files Args: - file_ids: The unique identifier of the files to be retried. + file_ids: The unique identifier of the files to retry. extra_headers: Send extra headers @@ -500,7 +500,7 @@ async def retry( Retry failed files Args: - file_ids: The unique identifier of the files to be retried. + file_ids: The unique identifier of the files to retry. extra_headers: Send extra headers diff --git a/src/writerai/resources/graphs.py b/src/writerai/resources/graphs.py index 6e369c7f..0265e501 100644 --- a/src/writerai/resources/graphs.py +++ b/src/writerai/resources/graphs.py @@ -337,9 +337,9 @@ def question( Ask a question to specified Knowledge Graphs. Args: - graph_ids: The unique identifiers of the Knowledge Graphs to be queried. + graph_ids: The unique identifiers of the Knowledge Graphs to query. - question: The question to be answered using the Knowledge Graph. + question: The question to answer using the Knowledge Graph. stream: Determines whether the model's output should be streamed. If true, the output is generated and sent incrementally, which can be useful for real-time @@ -376,9 +376,9 @@ def question( Ask a question to specified Knowledge Graphs. Args: - graph_ids: The unique identifiers of the Knowledge Graphs to be queried. + graph_ids: The unique identifiers of the Knowledge Graphs to query. - question: The question to be answered using the Knowledge Graph. + question: The question to answer using the Knowledge Graph. stream: Determines whether the model's output should be streamed. If true, the output is generated and sent incrementally, which can be useful for real-time @@ -415,9 +415,9 @@ def question( Ask a question to specified Knowledge Graphs. Args: - graph_ids: The unique identifiers of the Knowledge Graphs to be queried. + graph_ids: The unique identifiers of the Knowledge Graphs to query. - question: The question to be answered using the Knowledge Graph. + question: The question to answer using the Knowledge Graph. stream: Determines whether the model's output should be streamed. If true, the output is generated and sent incrementally, which can be useful for real-time @@ -804,9 +804,9 @@ async def question( Ask a question to specified Knowledge Graphs. Args: - graph_ids: The unique identifiers of the Knowledge Graphs to be queried. + graph_ids: The unique identifiers of the Knowledge Graphs to query. - question: The question to be answered using the Knowledge Graph. + question: The question to answer using the Knowledge Graph. stream: Determines whether the model's output should be streamed. If true, the output is generated and sent incrementally, which can be useful for real-time @@ -843,9 +843,9 @@ async def question( Ask a question to specified Knowledge Graphs. Args: - graph_ids: The unique identifiers of the Knowledge Graphs to be queried. + graph_ids: The unique identifiers of the Knowledge Graphs to query. - question: The question to be answered using the Knowledge Graph. + question: The question to answer using the Knowledge Graph. stream: Determines whether the model's output should be streamed. If true, the output is generated and sent incrementally, which can be useful for real-time @@ -882,9 +882,9 @@ async def question( Ask a question to specified Knowledge Graphs. Args: - graph_ids: The unique identifiers of the Knowledge Graphs to be queried. + graph_ids: The unique identifiers of the Knowledge Graphs to query. - question: The question to be answered using the Knowledge Graph. + question: The question to answer using the Knowledge Graph. stream: Determines whether the model's output should be streamed. If true, the output is generated and sent incrementally, which can be useful for real-time diff --git a/src/writerai/resources/tools/comprehend.py b/src/writerai/resources/tools/comprehend.py index 9d9b8470..b1607b0e 100644 --- a/src/writerai/resources/tools/comprehend.py +++ b/src/writerai/resources/tools/comprehend.py @@ -63,11 +63,11 @@ def medical( medical codes and confidence scores. Args: - content: The text to be analyzed. + content: The text to analyze. - response_type: The structure of the response to be returned. `Entities` returns medical - entities, `RxNorm` returns medication information, `ICD-10-CM` returns diagnosis - codes, and `SNOMED CT` returns medical concepts. + response_type: The structure of the response to return. `Entities` returns medical entities, + `RxNorm` returns medication information, `ICD-10-CM` returns diagnosis codes, + and `SNOMED CT` returns medical concepts. extra_headers: Send extra headers @@ -130,11 +130,11 @@ async def medical( medical codes and confidence scores. Args: - content: The text to be analyzed. + content: The text to analyze. - response_type: The structure of the response to be returned. `Entities` returns medical - entities, `RxNorm` returns medication information, `ICD-10-CM` returns diagnosis - codes, and `SNOMED CT` returns medical concepts. + response_type: The structure of the response to return. `Entities` returns medical entities, + `RxNorm` returns medication information, `ICD-10-CM` returns diagnosis codes, + and `SNOMED CT` returns medical concepts. extra_headers: Send extra headers diff --git a/src/writerai/resources/tools/tools.py b/src/writerai/resources/tools/tools.py index fa6f6414..9fd5e7f5 100644 --- a/src/writerai/resources/tools/tools.py +++ b/src/writerai/resources/tools/tools.py @@ -76,11 +76,11 @@ def context_aware_splitting( preserving the semantic meaning of the text and context between the chunks. Args: - strategy: The strategy to be used for splitting the text into chunks. `llm_split` uses the + strategy: The strategy to use for splitting the text into chunks. `llm_split` uses the language model to split the text, `fast_split` uses a fast heuristic-based approach, and `hybrid_split` combines both strategies. - text: The text to be split into chunks. + text: The text to split into chunks. extra_headers: Send extra headers @@ -184,11 +184,11 @@ async def context_aware_splitting( preserving the semantic meaning of the text and context between the chunks. Args: - strategy: The strategy to be used for splitting the text into chunks. `llm_split` uses the + strategy: The strategy to use for splitting the text into chunks. `llm_split` uses the language model to split the text, `fast_split` uses a fast heuristic-based approach, and `hybrid_split` combines both strategies. - text: The text to be split into chunks. + text: The text to split into chunks. extra_headers: Send extra headers diff --git a/src/writerai/resources/vision.py b/src/writerai/resources/vision.py index 26975788..db6c9861 100644 --- a/src/writerai/resources/vision.py +++ b/src/writerai/resources/vision.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Iterable +from typing_extensions import Literal import httpx @@ -49,7 +50,7 @@ def with_streaming_response(self) -> VisionResourceWithStreamingResponse: def analyze( self, *, - model: str, + model: Literal["palmyra-vision"], prompt: str, variables: Iterable[vision_analyze_params.Variable], # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -63,8 +64,7 @@ def analyze( Submit images and a prompt to generate an analysis of the images. Args: - model: The model to be used for image analysis. Currently only supports - `palmyra-vision`. + model: The model to use for image analysis. prompt: The prompt to use for the image analysis. The prompt must include the name of each image variable, surrounded by double curly braces (`{{}}`). For example, @@ -118,7 +118,7 @@ def with_streaming_response(self) -> AsyncVisionResourceWithStreamingResponse: async def analyze( self, *, - model: str, + model: Literal["palmyra-vision"], prompt: str, variables: Iterable[vision_analyze_params.Variable], # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -132,8 +132,7 @@ async def analyze( Submit images and a prompt to generate an analysis of the images. Args: - model: The model to be used for image analysis. Currently only supports - `palmyra-vision`. + model: The model to use for image analysis. prompt: The prompt to use for the image analysis. The prompt must include the name of each image variable, surrounded by double curly braces (`{{}}`). For example, diff --git a/src/writerai/types/application_generate_content_params.py b/src/writerai/types/application_generate_content_params.py index 9541512b..56b36867 100644 --- a/src/writerai/types/application_generate_content_params.py +++ b/src/writerai/types/application_generate_content_params.py @@ -31,10 +31,10 @@ class Input(TypedDict, total=False): If the input type is "File upload", you must pass the `file_id` of an uploaded file. You cannot pass a file object directly. See the - [file upload endpoint](/api-guides/api-reference/file-api/upload-files) for - instructions on uploading files or the - [list files endpoint](/api-guides/api-reference/file-api/get-all-files) for how - to see a list of uploaded files and their IDs. + [file upload endpoint](https://dev.writer.com/api-guides/api-reference/file-api/upload-files) + for instructions on uploading files or the + [list files endpoint](https://dev.writer.com/api-guides/api-reference/file-api/get-all-files) + for how to see a list of uploaded files and their IDs. """ diff --git a/src/writerai/types/applications/job_create_params.py b/src/writerai/types/applications/job_create_params.py index 467d133a..f0a908cd 100644 --- a/src/writerai/types/applications/job_create_params.py +++ b/src/writerai/types/applications/job_create_params.py @@ -27,8 +27,8 @@ class Input(TypedDict, total=False): If the input type is "File upload", you must pass the `file_id` of an uploaded file. You cannot pass a file object directly. See the - [file upload endpoint](/api-guides/api-reference/file-api/upload-files) for - instructions on uploading files or the - [list files endpoint](/api-guides/api-reference/file-api/get-all-files) for how - to see a list of uploaded files and their IDs. + [file upload endpoint](https://dev.writer.com/api-guides/api-reference/file-api/upload-files) + for instructions on uploading files or the + [list files endpoint](https://dev.writer.com/api-guides/api-reference/file-api/get-all-files) + for how to see a list of uploaded files and their IDs. """ diff --git a/src/writerai/types/chat_chat_params.py b/src/writerai/types/chat_chat_params.py index 08dce8f2..848e8489 100644 --- a/src/writerai/types/chat_chat_params.py +++ b/src/writerai/types/chat_chat_params.py @@ -28,10 +28,12 @@ class ChatChatParamsBase(TypedDict, total=False): the model to respond to. The array must contain at least one message. """ - model: Required[str] - """Specifies the model to be used for generating responses. - - The chat model is always `palmyra-x-004` for conversational use. + model: Required[ + Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"] + ] + """ + The [ID of the model](https://dev.writer.com/home/models) to use for creating + the chat completion. """ logprobs: bool @@ -47,7 +49,7 @@ class ChatChatParamsBase(TypedDict, total=False): n: int """ Specifies the number of completions (responses) to generate from the model in a - single request. This parameter allows multiple responses to be generated, + single request. This parameter allows for generating multiple responses, offering a variety of potential replies from which to choose. """ @@ -81,7 +83,9 @@ class ChatChatParamsBase(TypedDict, total=False): generate responses. The tool definitions use JSON schema. You can define your own functions or use one of the built-in `graph`, `llm`, or `vision` tools. Note that you can only use one built-in tool type in the array (only one of `graph`, - `llm`, or `vision`). + `llm`, or `vision`). You can pass multiple custom + tools](https://dev.writer.com/api-guides/tool-calling) of type `function` in the + same request. """ top_p: float @@ -98,8 +102,9 @@ class Message(TypedDict, total=False): """The role of the chat message. You can provide a system prompt by setting the role to `system`, or specify that - a message is the result of a [tool call](/api-guides/tool-calling) by setting - the role to `tool`. + a message is the result of a + [tool call](https://dev.writer.com/api-guides/tool-calling) by setting the role + to `tool`. """ content: Optional[str] diff --git a/src/writerai/types/completion_create_params.py b/src/writerai/types/completion_create_params.py index 20ad406d..0b926268 100644 --- a/src/writerai/types/completion_create_params.py +++ b/src/writerai/types/completion_create_params.py @@ -9,8 +9,13 @@ class CompletionCreateParamsBase(TypedDict, total=False): - model: Required[str] - """The identifier of the model to be used for processing the request.""" + model: Required[ + Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"] + ] + """ + The [ID of the model](https://dev.writer.com/home/models) to use for generating + text. + """ prompt: Required[str] """The input text that the model will process to generate a response.""" diff --git a/src/writerai/types/file_retry_params.py b/src/writerai/types/file_retry_params.py index a086d374..2f17762c 100644 --- a/src/writerai/types/file_retry_params.py +++ b/src/writerai/types/file_retry_params.py @@ -10,4 +10,4 @@ class FileRetryParams(TypedDict, total=False): file_ids: Required[List[str]] - """The unique identifier of the files to be retried.""" + """The unique identifier of the files to retry.""" diff --git a/src/writerai/types/graph_question_params.py b/src/writerai/types/graph_question_params.py index 570cbbd6..b31390b6 100644 --- a/src/writerai/types/graph_question_params.py +++ b/src/writerai/types/graph_question_params.py @@ -10,10 +10,10 @@ class GraphQuestionParamsBase(TypedDict, total=False): graph_ids: Required[List[str]] - """The unique identifiers of the Knowledge Graphs to be queried.""" + """The unique identifiers of the Knowledge Graphs to query.""" question: Required[str] - """The question to be answered using the Knowledge Graph.""" + """The question to answer using the Knowledge Graph.""" subqueries: bool """Specify whether to include subqueries.""" diff --git a/src/writerai/types/model_list_response.py b/src/writerai/types/model_list_response.py index cdec7b22..7393a6b2 100644 --- a/src/writerai/types/model_list_response.py +++ b/src/writerai/types/model_list_response.py @@ -17,4 +17,7 @@ class Model(BaseModel): class ModelListResponse(BaseModel): models: List[Model] - """The identifier of the model to be used for processing the request.""" + """ + The [ID of the model](https://dev.writer.com/home/models) to use for processing + the request. + """ diff --git a/src/writerai/types/shared/tool_param.py b/src/writerai/types/shared/tool_param.py index ccc6cd45..3d03f382 100644 --- a/src/writerai/types/shared/tool_param.py +++ b/src/writerai/types/shared/tool_param.py @@ -30,7 +30,7 @@ class FunctionTool(BaseModel): class GraphToolFunction(BaseModel): graph_ids: List[str] - """An array of graph IDs to be used in the tool.""" + """An array of graph IDs to use in the tool.""" subqueries: bool """Boolean to indicate whether to include subqueries in the response.""" @@ -49,10 +49,10 @@ class GraphTool(BaseModel): class LlmToolFunction(BaseModel): description: str - """A description of the model to be used.""" + """A description of the model to use.""" model: str - """The model to be used.""" + """The model to use.""" class LlmTool(BaseModel): @@ -65,7 +65,7 @@ class LlmTool(BaseModel): class VisionToolFunctionVariable(BaseModel): file_id: str - """The File ID of the image to be analyzed. + """The File ID of the image to analyze. The file must be uploaded to the Writer platform before you use it with the Vision tool. @@ -82,8 +82,8 @@ class VisionToolFunctionVariable(BaseModel): class VisionToolFunction(BaseModel): - model: str - """The model to be used for image analysis. Must be `palmyra-vision`.""" + model: Literal["palmyra-vision"] + """The model to use for image analysis.""" variables: List[VisionToolFunctionVariable] diff --git a/src/writerai/types/shared_params/tool_param.py b/src/writerai/types/shared_params/tool_param.py index 7723c2f3..902b74f7 100644 --- a/src/writerai/types/shared_params/tool_param.py +++ b/src/writerai/types/shared_params/tool_param.py @@ -30,7 +30,7 @@ class FunctionTool(TypedDict, total=False): class GraphToolFunction(TypedDict, total=False): graph_ids: Required[List[str]] - """An array of graph IDs to be used in the tool.""" + """An array of graph IDs to use in the tool.""" subqueries: Required[bool] """Boolean to indicate whether to include subqueries in the response.""" @@ -49,10 +49,10 @@ class GraphTool(TypedDict, total=False): class LlmToolFunction(TypedDict, total=False): description: Required[str] - """A description of the model to be used.""" + """A description of the model to use.""" model: Required[str] - """The model to be used.""" + """The model to use.""" class LlmTool(TypedDict, total=False): @@ -65,7 +65,7 @@ class LlmTool(TypedDict, total=False): class VisionToolFunctionVariable(TypedDict, total=False): file_id: Required[str] - """The File ID of the image to be analyzed. + """The File ID of the image to analyze. The file must be uploaded to the Writer platform before you use it with the Vision tool. @@ -82,8 +82,8 @@ class VisionToolFunctionVariable(TypedDict, total=False): class VisionToolFunction(TypedDict, total=False): - model: Required[str] - """The model to be used for image analysis. Must be `palmyra-vision`.""" + model: Required[Literal["palmyra-vision"]] + """The model to use for image analysis.""" variables: Required[Iterable[VisionToolFunctionVariable]] diff --git a/src/writerai/types/tool_context_aware_splitting_params.py b/src/writerai/types/tool_context_aware_splitting_params.py index b9ad4b06..eb94d79d 100644 --- a/src/writerai/types/tool_context_aware_splitting_params.py +++ b/src/writerai/types/tool_context_aware_splitting_params.py @@ -9,11 +9,11 @@ class ToolContextAwareSplittingParams(TypedDict, total=False): strategy: Required[Literal["llm_split", "fast_split", "hybrid_split"]] - """The strategy to be used for splitting the text into chunks. + """The strategy to use for splitting the text into chunks. `llm_split` uses the language model to split the text, `fast_split` uses a fast heuristic-based approach, and `hybrid_split` combines both strategies. """ text: Required[str] - """The text to be split into chunks.""" + """The text to split into chunks.""" diff --git a/src/writerai/types/tools/comprehend_medical_params.py b/src/writerai/types/tools/comprehend_medical_params.py index 8418dc6a..6377654f 100644 --- a/src/writerai/types/tools/comprehend_medical_params.py +++ b/src/writerai/types/tools/comprehend_medical_params.py @@ -9,10 +9,10 @@ class ComprehendMedicalParams(TypedDict, total=False): content: Required[str] - """The text to be analyzed.""" + """The text to analyze.""" response_type: Required[Literal["Entities", "RxNorm", "ICD-10-CM", "SNOMED CT"]] - """The structure of the response to be returned. + """The structure of the response to return. `Entities` returns medical entities, `RxNorm` returns medication information, `ICD-10-CM` returns diagnosis codes, and `SNOMED CT` returns medical concepts. diff --git a/src/writerai/types/vision_analyze_params.py b/src/writerai/types/vision_analyze_params.py index 96b50b99..9dac31ac 100644 --- a/src/writerai/types/vision_analyze_params.py +++ b/src/writerai/types/vision_analyze_params.py @@ -3,17 +3,14 @@ from __future__ import annotations from typing import Iterable -from typing_extensions import Required, TypedDict +from typing_extensions import Literal, Required, TypedDict __all__ = ["VisionAnalyzeParams", "Variable"] class VisionAnalyzeParams(TypedDict, total=False): - model: Required[str] - """The model to be used for image analysis. - - Currently only supports `palmyra-vision`. - """ + model: Required[Literal["palmyra-vision"]] + """The model to use for image analysis.""" prompt: Required[str] """The prompt to use for the image analysis. @@ -28,7 +25,7 @@ class VisionAnalyzeParams(TypedDict, total=False): class Variable(TypedDict, total=False): file_id: Required[str] - """The File ID of the image to be analyzed. + """The File ID of the image to analyze. The file must be uploaded to the Writer platform before it can be used in a vision request. diff --git a/tests/api_resources/test_chat.py b/tests/api_resources/test_chat.py index c035240e..9269f03a 100644 --- a/tests/api_resources/test_chat.py +++ b/tests/api_resources/test_chat.py @@ -21,7 +21,7 @@ class TestChat: def test_method_chat_overload_1(self, client: Writer) -> None: chat = client.chat.chat( messages=[{"role": "user"}], - model="model", + model="palmyra-x-004", ) assert_matches_type(ChatCompletion, chat, path=["response"]) @@ -69,7 +69,7 @@ def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: ], } ], - model="model", + model="palmyra-x-004", logprobs=True, max_tokens=0, n=0, @@ -96,7 +96,7 @@ def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: def test_raw_response_chat_overload_1(self, client: Writer) -> None: response = client.chat.with_raw_response.chat( messages=[{"role": "user"}], - model="model", + model="palmyra-x-004", ) assert response.is_closed is True @@ -108,7 +108,7 @@ def test_raw_response_chat_overload_1(self, client: Writer) -> None: def test_streaming_response_chat_overload_1(self, client: Writer) -> None: with client.chat.with_streaming_response.chat( messages=[{"role": "user"}], - model="model", + model="palmyra-x-004", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -122,7 +122,7 @@ def test_streaming_response_chat_overload_1(self, client: Writer) -> None: def test_method_chat_overload_2(self, client: Writer) -> None: chat_stream = client.chat.chat( messages=[{"role": "user"}], - model="model", + model="palmyra-x-004", stream=True, ) chat_stream.response.close() @@ -171,7 +171,7 @@ def test_method_chat_with_all_params_overload_2(self, client: Writer) -> None: ], } ], - model="model", + model="palmyra-x-004", stream=True, logprobs=True, max_tokens=0, @@ -198,7 +198,7 @@ def test_method_chat_with_all_params_overload_2(self, client: Writer) -> None: def test_raw_response_chat_overload_2(self, client: Writer) -> None: response = client.chat.with_raw_response.chat( messages=[{"role": "user"}], - model="model", + model="palmyra-x-004", stream=True, ) @@ -210,7 +210,7 @@ def test_raw_response_chat_overload_2(self, client: Writer) -> None: def test_streaming_response_chat_overload_2(self, client: Writer) -> None: with client.chat.with_streaming_response.chat( messages=[{"role": "user"}], - model="model", + model="palmyra-x-004", stream=True, ) as response: assert not response.is_closed @@ -229,7 +229,7 @@ class TestAsyncChat: async def test_method_chat_overload_1(self, async_client: AsyncWriter) -> None: chat = await async_client.chat.chat( messages=[{"role": "user"}], - model="model", + model="palmyra-x-004", ) assert_matches_type(ChatCompletion, chat, path=["response"]) @@ -277,7 +277,7 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW ], } ], - model="model", + model="palmyra-x-004", logprobs=True, max_tokens=0, n=0, @@ -304,7 +304,7 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW async def test_raw_response_chat_overload_1(self, async_client: AsyncWriter) -> None: response = await async_client.chat.with_raw_response.chat( messages=[{"role": "user"}], - model="model", + model="palmyra-x-004", ) assert response.is_closed is True @@ -316,7 +316,7 @@ async def test_raw_response_chat_overload_1(self, async_client: AsyncWriter) -> async def test_streaming_response_chat_overload_1(self, async_client: AsyncWriter) -> None: async with async_client.chat.with_streaming_response.chat( messages=[{"role": "user"}], - model="model", + model="palmyra-x-004", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -330,7 +330,7 @@ async def test_streaming_response_chat_overload_1(self, async_client: AsyncWrite async def test_method_chat_overload_2(self, async_client: AsyncWriter) -> None: chat_stream = await async_client.chat.chat( messages=[{"role": "user"}], - model="model", + model="palmyra-x-004", stream=True, ) await chat_stream.response.aclose() @@ -379,7 +379,7 @@ async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncW ], } ], - model="model", + model="palmyra-x-004", stream=True, logprobs=True, max_tokens=0, @@ -406,7 +406,7 @@ async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncW async def test_raw_response_chat_overload_2(self, async_client: AsyncWriter) -> None: response = await async_client.chat.with_raw_response.chat( messages=[{"role": "user"}], - model="model", + model="palmyra-x-004", stream=True, ) @@ -418,7 +418,7 @@ async def test_raw_response_chat_overload_2(self, async_client: AsyncWriter) -> async def test_streaming_response_chat_overload_2(self, async_client: AsyncWriter) -> None: async with async_client.chat.with_streaming_response.chat( messages=[{"role": "user"}], - model="model", + model="palmyra-x-004", stream=True, ) as response: assert not response.is_closed diff --git a/tests/test_client.py b/tests/test_client.py index c43a200d..265babef 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -805,7 +805,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) - response = client.chat.with_raw_response.chat(messages=[{"role": "user"}], model="model") + response = client.chat.with_raw_response.chat(messages=[{"role": "user"}], model="palmyra-x-004") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -830,7 +830,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) response = client.chat.with_raw_response.chat( - messages=[{"role": "user"}], model="model", extra_headers={"x-stainless-retry-count": Omit()} + messages=[{"role": "user"}], model="palmyra-x-004", extra_headers={"x-stainless-retry-count": Omit()} ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -855,7 +855,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) response = client.chat.with_raw_response.chat( - messages=[{"role": "user"}], model="model", extra_headers={"x-stainless-retry-count": "42"} + messages=[{"role": "user"}], model="palmyra-x-004", extra_headers={"x-stainless-retry-count": "42"} ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" @@ -1626,7 +1626,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) - response = await client.chat.with_raw_response.chat(messages=[{"role": "user"}], model="model") + response = await client.chat.with_raw_response.chat(messages=[{"role": "user"}], model="palmyra-x-004") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -1652,7 +1652,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) response = await client.chat.with_raw_response.chat( - messages=[{"role": "user"}], model="model", extra_headers={"x-stainless-retry-count": Omit()} + messages=[{"role": "user"}], model="palmyra-x-004", extra_headers={"x-stainless-retry-count": Omit()} ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -1678,7 +1678,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) response = await client.chat.with_raw_response.chat( - messages=[{"role": "user"}], model="model", extra_headers={"x-stainless-retry-count": "42"} + messages=[{"role": "user"}], model="palmyra-x-004", extra_headers={"x-stainless-retry-count": "42"} ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" From 98ccc9d3594dc9a89647b7eda73de60993a7f467 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 21 Apr 2025 23:10:20 +0000 Subject: [PATCH 232/399] feat(api): add translation endpoint --- .stats.yml | 4 +- api.md | 12 + src/writerai/_client.py | 10 +- src/writerai/resources/__init__.py | 14 + src/writerai/resources/translation.py | 266 ++++++++++++++++++ src/writerai/types/__init__.py | 2 + src/writerai/types/translation_response.py | 11 + .../types/translation_translate_params.py | 61 ++++ tests/api_resources/test_translation.py | 120 ++++++++ 9 files changed, 497 insertions(+), 3 deletions(-) create mode 100644 src/writerai/resources/translation.py create mode 100644 src/writerai/types/translation_response.py create mode 100644 src/writerai/types/translation_translate_params.py create mode 100644 tests/api_resources/test_translation.py diff --git a/.stats.yml b/.stats.yml index 487e77f6..3a0b8aa2 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 30 +configured_endpoints: 31 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-a8e20e751972f7b8c948a833764b02d0835b380189eaf7393d63eb2250dbd598.yml openapi_spec_hash: 6f27335468bc5bf01fbcd1454330fb07 -config_hash: ee2e5a914fd298a6f4c740f5ce187f87 +config_hash: 1f6d0bf7309d0007e28ab85b89a0de85 diff --git a/api.md b/api.md index 5cb0e727..8a83ff66 100644 --- a/api.md +++ b/api.md @@ -182,6 +182,18 @@ Methods: - client.tools.comprehend.medical(\*\*params) -> ComprehendMedicalResponse +# Translation + +Types: + +```python +from writerai.types import TranslationRequest, TranslationResponse +``` + +Methods: + +- client.translation.translate(\*\*params) -> TranslationResponse + # Vision Types: diff --git a/src/writerai/_client.py b/src/writerai/_client.py index 6f359a41..78e2511f 100644 --- a/src/writerai/_client.py +++ b/src/writerai/_client.py @@ -24,7 +24,7 @@ get_async_library, ) from ._version import __version__ -from .resources import chat, files, graphs, models, vision, completions +from .resources import chat, files, graphs, models, vision, completions, translation from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import WriterError, APIStatusError from ._base_client import ( @@ -46,6 +46,7 @@ class Writer(SyncAPIClient): graphs: graphs.GraphsResource files: files.FilesResource tools: tools.ToolsResource + translation: translation.TranslationResource vision: vision.VisionResource with_raw_response: WriterWithRawResponse with_streaming_response: WriterWithStreamedResponse @@ -113,6 +114,7 @@ def __init__( self.graphs = graphs.GraphsResource(self) self.files = files.FilesResource(self) self.tools = tools.ToolsResource(self) + self.translation = translation.TranslationResource(self) self.vision = vision.VisionResource(self) self.with_raw_response = WriterWithRawResponse(self) self.with_streaming_response = WriterWithStreamedResponse(self) @@ -230,6 +232,7 @@ class AsyncWriter(AsyncAPIClient): graphs: graphs.AsyncGraphsResource files: files.AsyncFilesResource tools: tools.AsyncToolsResource + translation: translation.AsyncTranslationResource vision: vision.AsyncVisionResource with_raw_response: AsyncWriterWithRawResponse with_streaming_response: AsyncWriterWithStreamedResponse @@ -297,6 +300,7 @@ def __init__( self.graphs = graphs.AsyncGraphsResource(self) self.files = files.AsyncFilesResource(self) self.tools = tools.AsyncToolsResource(self) + self.translation = translation.AsyncTranslationResource(self) self.vision = vision.AsyncVisionResource(self) self.with_raw_response = AsyncWriterWithRawResponse(self) self.with_streaming_response = AsyncWriterWithStreamedResponse(self) @@ -415,6 +419,7 @@ def __init__(self, client: Writer) -> None: self.graphs = graphs.GraphsResourceWithRawResponse(client.graphs) self.files = files.FilesResourceWithRawResponse(client.files) self.tools = tools.ToolsResourceWithRawResponse(client.tools) + self.translation = translation.TranslationResourceWithRawResponse(client.translation) self.vision = vision.VisionResourceWithRawResponse(client.vision) @@ -427,6 +432,7 @@ def __init__(self, client: AsyncWriter) -> None: self.graphs = graphs.AsyncGraphsResourceWithRawResponse(client.graphs) self.files = files.AsyncFilesResourceWithRawResponse(client.files) self.tools = tools.AsyncToolsResourceWithRawResponse(client.tools) + self.translation = translation.AsyncTranslationResourceWithRawResponse(client.translation) self.vision = vision.AsyncVisionResourceWithRawResponse(client.vision) @@ -439,6 +445,7 @@ def __init__(self, client: Writer) -> None: self.graphs = graphs.GraphsResourceWithStreamingResponse(client.graphs) self.files = files.FilesResourceWithStreamingResponse(client.files) self.tools = tools.ToolsResourceWithStreamingResponse(client.tools) + self.translation = translation.TranslationResourceWithStreamingResponse(client.translation) self.vision = vision.VisionResourceWithStreamingResponse(client.vision) @@ -451,6 +458,7 @@ def __init__(self, client: AsyncWriter) -> None: self.graphs = graphs.AsyncGraphsResourceWithStreamingResponse(client.graphs) self.files = files.AsyncFilesResourceWithStreamingResponse(client.files) self.tools = tools.AsyncToolsResourceWithStreamingResponse(client.tools) + self.translation = translation.AsyncTranslationResourceWithStreamingResponse(client.translation) self.vision = vision.AsyncVisionResourceWithStreamingResponse(client.vision) diff --git a/src/writerai/resources/__init__.py b/src/writerai/resources/__init__.py index 40417904..767c0120 100644 --- a/src/writerai/resources/__init__.py +++ b/src/writerai/resources/__init__.py @@ -56,6 +56,14 @@ CompletionsResourceWithStreamingResponse, AsyncCompletionsResourceWithStreamingResponse, ) +from .translation import ( + TranslationResource, + AsyncTranslationResource, + TranslationResourceWithRawResponse, + AsyncTranslationResourceWithRawResponse, + TranslationResourceWithStreamingResponse, + AsyncTranslationResourceWithStreamingResponse, +) from .applications import ( ApplicationsResource, AsyncApplicationsResource, @@ -108,6 +116,12 @@ "AsyncToolsResourceWithRawResponse", "ToolsResourceWithStreamingResponse", "AsyncToolsResourceWithStreamingResponse", + "TranslationResource", + "AsyncTranslationResource", + "TranslationResourceWithRawResponse", + "AsyncTranslationResourceWithRawResponse", + "TranslationResourceWithStreamingResponse", + "AsyncTranslationResourceWithStreamingResponse", "VisionResource", "AsyncVisionResource", "VisionResourceWithRawResponse", diff --git a/src/writerai/resources/translation.py b/src/writerai/resources/translation.py new file mode 100644 index 00000000..c72f287b --- /dev/null +++ b/src/writerai/resources/translation.py @@ -0,0 +1,266 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal + +import httpx + +from ..types import translation_translate_params +from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from .._utils import ( + maybe_transform, + async_maybe_transform, +) +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.translation_response import TranslationResponse + +__all__ = ["TranslationResource", "AsyncTranslationResource"] + + +class TranslationResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> TranslationResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers + """ + return TranslationResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> TranslationResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/writer/writer-python#with_streaming_response + """ + return TranslationResourceWithStreamingResponse(self) + + def translate( + self, + *, + formality: bool, + length_control: bool, + mask_profanity: bool, + model: Literal["palmyra-translate"], + source_language_code: str, + target_language_code: str, + text: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TranslationResponse: + """ + Translate text from one language to another. + + Args: + formality: Whether to use formal or informal language in the translation. See the + [list of languages that support formality](https://dev.writer.com/api-guides/api-reference/translation-api/language-support#formality). + If the language does not support formality, this parameter is ignored. + + length_control: Whether to control the length of the translated text. See the + [list of languages that support length control](https://dev.writer.com/api-guides/api-reference/translation-api/language-support#length-control). + If the language does not support length control, this parameter is ignored. + + mask_profanity: Whether to mask profane words in the translated text. See the + [list of languages that do not support profanity masking](https://dev.writer.com/api-guides/api-reference/translation-api/language-support#profanity-masking). + If the language does not support profanity masking, this parameter is ignored. + + model: The model to use for translation. + + source_language_code: The [ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes) + language code of the original text to translate. For example, `en` for English, + `zh` for Chinese, `fr` for French, `es` for Spanish. If the language has a + variant, the code appends the two-digit + [ISO-3166 country code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes). + For example, Mexican Spanish is `es-MX`. See the + [list of supported languages and language codes](https://dev.writer.com/api-guides/api-reference/translation-api/language-support). + + target_language_code: The [ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes) + language code of the target language for the translation. For example, `en` for + English, `zh` for Chinese, `fr` for French, `es` for Spanish. If the language + has a variant, the code appends the two-digit + [ISO-3166 country code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes). + For example, Mexican Spanish is `es-MX`. See the + [list of supported languages and language codes](https://dev.writer.com/api-guides/api-reference/translation-api/language-support). + + text: The text to translate. Maximum of 100,000 words. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/v1/translation", + body=maybe_transform( + { + "formality": formality, + "length_control": length_control, + "mask_profanity": mask_profanity, + "model": model, + "source_language_code": source_language_code, + "target_language_code": target_language_code, + "text": text, + }, + translation_translate_params.TranslationTranslateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=TranslationResponse, + ) + + +class AsyncTranslationResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncTranslationResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers + """ + return AsyncTranslationResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncTranslationResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/writer/writer-python#with_streaming_response + """ + return AsyncTranslationResourceWithStreamingResponse(self) + + async def translate( + self, + *, + formality: bool, + length_control: bool, + mask_profanity: bool, + model: Literal["palmyra-translate"], + source_language_code: str, + target_language_code: str, + text: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TranslationResponse: + """ + Translate text from one language to another. + + Args: + formality: Whether to use formal or informal language in the translation. See the + [list of languages that support formality](https://dev.writer.com/api-guides/api-reference/translation-api/language-support#formality). + If the language does not support formality, this parameter is ignored. + + length_control: Whether to control the length of the translated text. See the + [list of languages that support length control](https://dev.writer.com/api-guides/api-reference/translation-api/language-support#length-control). + If the language does not support length control, this parameter is ignored. + + mask_profanity: Whether to mask profane words in the translated text. See the + [list of languages that do not support profanity masking](https://dev.writer.com/api-guides/api-reference/translation-api/language-support#profanity-masking). + If the language does not support profanity masking, this parameter is ignored. + + model: The model to use for translation. + + source_language_code: The [ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes) + language code of the original text to translate. For example, `en` for English, + `zh` for Chinese, `fr` for French, `es` for Spanish. If the language has a + variant, the code appends the two-digit + [ISO-3166 country code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes). + For example, Mexican Spanish is `es-MX`. See the + [list of supported languages and language codes](https://dev.writer.com/api-guides/api-reference/translation-api/language-support). + + target_language_code: The [ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes) + language code of the target language for the translation. For example, `en` for + English, `zh` for Chinese, `fr` for French, `es` for Spanish. If the language + has a variant, the code appends the two-digit + [ISO-3166 country code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes). + For example, Mexican Spanish is `es-MX`. See the + [list of supported languages and language codes](https://dev.writer.com/api-guides/api-reference/translation-api/language-support). + + text: The text to translate. Maximum of 100,000 words. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/v1/translation", + body=await async_maybe_transform( + { + "formality": formality, + "length_control": length_control, + "mask_profanity": mask_profanity, + "model": model, + "source_language_code": source_language_code, + "target_language_code": target_language_code, + "text": text, + }, + translation_translate_params.TranslationTranslateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=TranslationResponse, + ) + + +class TranslationResourceWithRawResponse: + def __init__(self, translation: TranslationResource) -> None: + self._translation = translation + + self.translate = to_raw_response_wrapper( + translation.translate, + ) + + +class AsyncTranslationResourceWithRawResponse: + def __init__(self, translation: AsyncTranslationResource) -> None: + self._translation = translation + + self.translate = async_to_raw_response_wrapper( + translation.translate, + ) + + +class TranslationResourceWithStreamingResponse: + def __init__(self, translation: TranslationResource) -> None: + self._translation = translation + + self.translate = to_streamed_response_wrapper( + translation.translate, + ) + + +class AsyncTranslationResourceWithStreamingResponse: + def __init__(self, translation: AsyncTranslationResource) -> None: + self._translation = translation + + self.translate = async_to_streamed_response_wrapper( + translation.translate, + ) diff --git a/src/writerai/types/__init__.py b/src/writerai/types/__init__.py index 03f63434..aabbde13 100644 --- a/src/writerai/types/__init__.py +++ b/src/writerai/types/__init__.py @@ -34,6 +34,7 @@ from .graph_update_params import GraphUpdateParams as GraphUpdateParams from .model_list_response import ModelListResponse as ModelListResponse from .file_delete_response import FileDeleteResponse as FileDeleteResponse +from .translation_response import TranslationResponse as TranslationResponse from .chat_completion_chunk import ChatCompletionChunk as ChatCompletionChunk from .chat_completion_usage import ChatCompletionUsage as ChatCompletionUsage from .graph_create_response import GraphCreateResponse as GraphCreateResponse @@ -49,6 +50,7 @@ from .tool_parse_pdf_response import ToolParsePdfResponse as ToolParsePdfResponse from .completion_create_params import CompletionCreateParams as CompletionCreateParams from .application_list_response import ApplicationListResponse as ApplicationListResponse +from .translation_translate_params import TranslationTranslateParams as TranslationTranslateParams from .application_retrieve_response import ApplicationRetrieveResponse as ApplicationRetrieveResponse from .graph_add_file_to_graph_params import GraphAddFileToGraphParams as GraphAddFileToGraphParams from .application_generate_content_chunk import ApplicationGenerateContentChunk as ApplicationGenerateContentChunk diff --git a/src/writerai/types/translation_response.py b/src/writerai/types/translation_response.py new file mode 100644 index 00000000..bbd2fee3 --- /dev/null +++ b/src/writerai/types/translation_response.py @@ -0,0 +1,11 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + + +from .._models import BaseModel + +__all__ = ["TranslationResponse"] + + +class TranslationResponse(BaseModel): + data: str + """The result of the translation.""" diff --git a/src/writerai/types/translation_translate_params.py b/src/writerai/types/translation_translate_params.py new file mode 100644 index 00000000..636618f1 --- /dev/null +++ b/src/writerai/types/translation_translate_params.py @@ -0,0 +1,61 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["TranslationTranslateParams"] + + +class TranslationTranslateParams(TypedDict, total=False): + formality: Required[bool] + """Whether to use formal or informal language in the translation. + + See the + [list of languages that support formality](https://dev.writer.com/api-guides/api-reference/translation-api/language-support#formality). + If the language does not support formality, this parameter is ignored. + """ + + length_control: Required[bool] + """Whether to control the length of the translated text. + + See the + [list of languages that support length control](https://dev.writer.com/api-guides/api-reference/translation-api/language-support#length-control). + If the language does not support length control, this parameter is ignored. + """ + + mask_profanity: Required[bool] + """Whether to mask profane words in the translated text. + + See the + [list of languages that do not support profanity masking](https://dev.writer.com/api-guides/api-reference/translation-api/language-support#profanity-masking). + If the language does not support profanity masking, this parameter is ignored. + """ + + model: Required[Literal["palmyra-translate"]] + """The model to use for translation.""" + + source_language_code: Required[str] + """ + The [ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes) + language code of the original text to translate. For example, `en` for English, + `zh` for Chinese, `fr` for French, `es` for Spanish. If the language has a + variant, the code appends the two-digit + [ISO-3166 country code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes). + For example, Mexican Spanish is `es-MX`. See the + [list of supported languages and language codes](https://dev.writer.com/api-guides/api-reference/translation-api/language-support). + """ + + target_language_code: Required[str] + """ + The [ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes) + language code of the target language for the translation. For example, `en` for + English, `zh` for Chinese, `fr` for French, `es` for Spanish. If the language + has a variant, the code appends the two-digit + [ISO-3166 country code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes). + For example, Mexican Spanish is `es-MX`. See the + [list of supported languages and language codes](https://dev.writer.com/api-guides/api-reference/translation-api/language-support). + """ + + text: Required[str] + """The text to translate. Maximum of 100,000 words.""" diff --git a/tests/api_resources/test_translation.py b/tests/api_resources/test_translation.py new file mode 100644 index 00000000..caa3296f --- /dev/null +++ b/tests/api_resources/test_translation.py @@ -0,0 +1,120 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from writerai import Writer, AsyncWriter +from tests.utils import assert_matches_type +from writerai.types import TranslationResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestTranslation: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_translate(self, client: Writer) -> None: + translation = client.translation.translate( + formality=True, + length_control=True, + mask_profanity=True, + model="palmyra-translate", + source_language_code="en", + target_language_code="es", + text="Hello, world!", + ) + assert_matches_type(TranslationResponse, translation, path=["response"]) + + @parametrize + def test_raw_response_translate(self, client: Writer) -> None: + response = client.translation.with_raw_response.translate( + formality=True, + length_control=True, + mask_profanity=True, + model="palmyra-translate", + source_language_code="en", + target_language_code="es", + text="Hello, world!", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + translation = response.parse() + assert_matches_type(TranslationResponse, translation, path=["response"]) + + @parametrize + def test_streaming_response_translate(self, client: Writer) -> None: + with client.translation.with_streaming_response.translate( + formality=True, + length_control=True, + mask_profanity=True, + model="palmyra-translate", + source_language_code="en", + target_language_code="es", + text="Hello, world!", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + translation = response.parse() + assert_matches_type(TranslationResponse, translation, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncTranslation: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_translate(self, async_client: AsyncWriter) -> None: + translation = await async_client.translation.translate( + formality=True, + length_control=True, + mask_profanity=True, + model="palmyra-translate", + source_language_code="en", + target_language_code="es", + text="Hello, world!", + ) + assert_matches_type(TranslationResponse, translation, path=["response"]) + + @parametrize + async def test_raw_response_translate(self, async_client: AsyncWriter) -> None: + response = await async_client.translation.with_raw_response.translate( + formality=True, + length_control=True, + mask_profanity=True, + model="palmyra-translate", + source_language_code="en", + target_language_code="es", + text="Hello, world!", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + translation = await response.parse() + assert_matches_type(TranslationResponse, translation, path=["response"]) + + @parametrize + async def test_streaming_response_translate(self, async_client: AsyncWriter) -> None: + async with async_client.translation.with_streaming_response.translate( + formality=True, + length_control=True, + mask_profanity=True, + model="palmyra-translate", + source_language_code="en", + target_language_code="es", + text="Hello, world!", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + translation = await response.parse() + assert_matches_type(TranslationResponse, translation, path=["response"]) + + assert cast(Any, response.is_closed) is True From 11aa5aa5bc9f7e60328072ddc95dc1354d43e063 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 22 Apr 2025 20:07:35 +0000 Subject: [PATCH 233/399] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 3a0b8aa2..5e11c3db 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 31 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-a8e20e751972f7b8c948a833764b02d0835b380189eaf7393d63eb2250dbd598.yml -openapi_spec_hash: 6f27335468bc5bf01fbcd1454330fb07 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-8de5485a6892144049ab388a3dc5378c3319cbbe551c3db1e512bb620ab414ec.yml +openapi_spec_hash: c93452a50c9ef635a5fae65830c3cc81 config_hash: 1f6d0bf7309d0007e28ab85b89a0de85 From 9d3ee21c2b0bba18216dc438912217deb81f348f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 22 Apr 2025 20:13:12 +0000 Subject: [PATCH 234/399] chore(ci): add timeout thresholds for CI jobs --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 81f6dc20..04b083ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,7 @@ on: jobs: lint: + timeout-minutes: 10 name: lint runs-on: ubuntu-latest steps: @@ -30,6 +31,7 @@ jobs: run: ./scripts/lint test: + timeout-minutes: 10 name: test runs-on: ubuntu-latest steps: From e182d2d426fe29fd6056ee1381e6a0dddfe0e875 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 22 Apr 2025 20:36:42 +0000 Subject: [PATCH 235/399] chore(internal): import reformatting --- src/writerai/_client.py | 5 +---- src/writerai/resources/applications/applications.py | 6 +----- src/writerai/resources/applications/graphs.py | 5 +---- src/writerai/resources/applications/jobs.py | 5 +---- src/writerai/resources/chat.py | 6 +----- src/writerai/resources/completions.py | 6 +----- src/writerai/resources/files.py | 5 +---- src/writerai/resources/graphs.py | 6 +----- src/writerai/resources/tools/comprehend.py | 5 +---- src/writerai/resources/tools/tools.py | 5 +---- src/writerai/resources/translation.py | 5 +---- src/writerai/resources/vision.py | 5 +---- 12 files changed, 12 insertions(+), 52 deletions(-) diff --git a/src/writerai/_client.py b/src/writerai/_client.py index 78e2511f..a58c7fca 100644 --- a/src/writerai/_client.py +++ b/src/writerai/_client.py @@ -19,10 +19,7 @@ ProxiesTypes, RequestOptions, ) -from ._utils import ( - is_given, - get_async_library, -) +from ._utils import is_given, get_async_library from ._version import __version__ from .resources import chat, files, graphs, models, vision, completions, translation from ._streaming import Stream as Stream, AsyncStream as AsyncStream diff --git a/src/writerai/resources/applications/applications.py b/src/writerai/resources/applications/applications.py index b8f5010d..4a0e5f6d 100644 --- a/src/writerai/resources/applications/applications.py +++ b/src/writerai/resources/applications/applications.py @@ -25,11 +25,7 @@ ) from ...types import application_list_params, application_generate_content_params from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from ..._utils import ( - required_args, - maybe_transform, - async_maybe_transform, -) +from ..._utils import required_args, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( diff --git a/src/writerai/resources/applications/graphs.py b/src/writerai/resources/applications/graphs.py index 0444346f..e87e147a 100644 --- a/src/writerai/resources/applications/graphs.py +++ b/src/writerai/resources/applications/graphs.py @@ -7,10 +7,7 @@ import httpx from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from ..._utils import ( - maybe_transform, - async_maybe_transform, -) +from ..._utils import maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( diff --git a/src/writerai/resources/applications/jobs.py b/src/writerai/resources/applications/jobs.py index b66e40d0..6a277cc0 100644 --- a/src/writerai/resources/applications/jobs.py +++ b/src/writerai/resources/applications/jobs.py @@ -8,10 +8,7 @@ import httpx from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from ..._utils import ( - maybe_transform, - async_maybe_transform, -) +from ..._utils import maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( diff --git a/src/writerai/resources/chat.py b/src/writerai/resources/chat.py index 8d264f2f..0cce886a 100644 --- a/src/writerai/resources/chat.py +++ b/src/writerai/resources/chat.py @@ -9,11 +9,7 @@ from ..types import chat_chat_params from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from .._utils import ( - required_args, - maybe_transform, - async_maybe_transform, -) +from .._utils import required_args, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( diff --git a/src/writerai/resources/completions.py b/src/writerai/resources/completions.py index 6e9357cf..87141ed0 100644 --- a/src/writerai/resources/completions.py +++ b/src/writerai/resources/completions.py @@ -9,11 +9,7 @@ from ..types import completion_create_params from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from .._utils import ( - required_args, - maybe_transform, - async_maybe_transform, -) +from .._utils import required_args, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( diff --git a/src/writerai/resources/files.py b/src/writerai/resources/files.py index 2f530cd7..3cc4a746 100644 --- a/src/writerai/resources/files.py +++ b/src/writerai/resources/files.py @@ -9,10 +9,7 @@ from ..types import file_list_params, file_retry_params, file_upload_params from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes -from .._utils import ( - maybe_transform, - async_maybe_transform, -) +from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( diff --git a/src/writerai/resources/graphs.py b/src/writerai/resources/graphs.py index 0265e501..e93b2a67 100644 --- a/src/writerai/resources/graphs.py +++ b/src/writerai/resources/graphs.py @@ -15,11 +15,7 @@ graph_add_file_to_graph_params, ) from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from .._utils import ( - required_args, - maybe_transform, - async_maybe_transform, -) +from .._utils import required_args, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( diff --git a/src/writerai/resources/tools/comprehend.py b/src/writerai/resources/tools/comprehend.py index b1607b0e..cbae676a 100644 --- a/src/writerai/resources/tools/comprehend.py +++ b/src/writerai/resources/tools/comprehend.py @@ -7,10 +7,7 @@ import httpx from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from ..._utils import ( - maybe_transform, - async_maybe_transform, -) +from ..._utils import maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( diff --git a/src/writerai/resources/tools/tools.py b/src/writerai/resources/tools/tools.py index 9fd5e7f5..ad2d6c1e 100644 --- a/src/writerai/resources/tools/tools.py +++ b/src/writerai/resources/tools/tools.py @@ -8,10 +8,7 @@ from ...types import tool_parse_pdf_params, tool_context_aware_splitting_params from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from ..._utils import ( - maybe_transform, - async_maybe_transform, -) +from ..._utils import maybe_transform, async_maybe_transform from ..._compat import cached_property from .comprehend import ( ComprehendResource, diff --git a/src/writerai/resources/translation.py b/src/writerai/resources/translation.py index c72f287b..f342bb6c 100644 --- a/src/writerai/resources/translation.py +++ b/src/writerai/resources/translation.py @@ -8,10 +8,7 @@ from ..types import translation_translate_params from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from .._utils import ( - maybe_transform, - async_maybe_transform, -) +from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( diff --git a/src/writerai/resources/vision.py b/src/writerai/resources/vision.py index db6c9861..0bea951f 100644 --- a/src/writerai/resources/vision.py +++ b/src/writerai/resources/vision.py @@ -9,10 +9,7 @@ from ..types import vision_analyze_params from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from .._utils import ( - maybe_transform, - async_maybe_transform, -) +from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( From a90ec987f51a11c455638d5dd7831f1bb84b3936 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 22 Apr 2025 22:04:01 +0000 Subject: [PATCH 236/399] chore(internal): fix list file params --- src/writerai/_utils/_utils.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/writerai/_utils/_utils.py b/src/writerai/_utils/_utils.py index e5811bba..ea3cf3f2 100644 --- a/src/writerai/_utils/_utils.py +++ b/src/writerai/_utils/_utils.py @@ -72,8 +72,16 @@ def _extract_items( from .._files import assert_is_file_content # We have exhausted the path, return the entry we found. - assert_is_file_content(obj, key=flattened_key) assert flattened_key is not None + + if is_list(obj): + files: list[tuple[str, FileTypes]] = [] + for entry in obj: + assert_is_file_content(entry, key=flattened_key + "[]" if flattened_key else "") + files.append((flattened_key + "[]", cast(FileTypes, entry))) + return files + + assert_is_file_content(obj, key=flattened_key) return [(flattened_key, cast(FileTypes, obj))] index += 1 From 14e5301a83e5bcac0fb5a77b41cdc03f5487062d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 22 Apr 2025 22:55:41 +0000 Subject: [PATCH 237/399] chore(internal): refactor retries to not use recursion --- src/writerai/_base_client.py | 414 +++++++++++++++-------------------- 1 file changed, 175 insertions(+), 239 deletions(-) diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index c055609b..078b2770 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -437,8 +437,7 @@ def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0 headers = httpx.Headers(headers_dict) idempotency_header = self._idempotency_header - if idempotency_header and options.method.lower() != "get" and idempotency_header not in headers: - options.idempotency_key = options.idempotency_key or self._idempotency_key() + if idempotency_header and options.idempotency_key and idempotency_header not in headers: headers[idempotency_header] = options.idempotency_key # Don't set these headers if they were already set or removed by the caller. We check @@ -903,7 +902,6 @@ def request( self, cast_to: Type[ResponseT], options: FinalRequestOptions, - remaining_retries: Optional[int] = None, *, stream: Literal[True], stream_cls: Type[_StreamT], @@ -914,7 +912,6 @@ def request( self, cast_to: Type[ResponseT], options: FinalRequestOptions, - remaining_retries: Optional[int] = None, *, stream: Literal[False] = False, ) -> ResponseT: ... @@ -924,7 +921,6 @@ def request( self, cast_to: Type[ResponseT], options: FinalRequestOptions, - remaining_retries: Optional[int] = None, *, stream: bool = False, stream_cls: Type[_StreamT] | None = None, @@ -934,125 +930,109 @@ def request( self, cast_to: Type[ResponseT], options: FinalRequestOptions, - remaining_retries: Optional[int] = None, *, stream: bool = False, stream_cls: type[_StreamT] | None = None, ) -> ResponseT | _StreamT: - if remaining_retries is not None: - retries_taken = options.get_max_retries(self.max_retries) - remaining_retries - else: - retries_taken = 0 - - return self._request( - cast_to=cast_to, - options=options, - stream=stream, - stream_cls=stream_cls, - retries_taken=retries_taken, - ) + cast_to = self._maybe_override_cast_to(cast_to, options) - def _request( - self, - *, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - retries_taken: int, - stream: bool, - stream_cls: type[_StreamT] | None, - ) -> ResponseT | _StreamT: # create a copy of the options we were given so that if the # options are mutated later & we then retry, the retries are # given the original options input_options = model_copy(options) - - cast_to = self._maybe_override_cast_to(cast_to, options) - options = self._prepare_options(options) - - remaining_retries = options.get_max_retries(self.max_retries) - retries_taken - request = self._build_request(options, retries_taken=retries_taken) - self._prepare_request(request) - - if options.idempotency_key: + if input_options.idempotency_key is None and input_options.method.lower() != "get": # ensure the idempotency key is reused between requests - input_options.idempotency_key = options.idempotency_key + input_options.idempotency_key = self._idempotency_key() - kwargs: HttpxSendArgs = {} - if self.custom_auth is not None: - kwargs["auth"] = self.custom_auth + response: httpx.Response | None = None + max_retries = input_options.get_max_retries(self.max_retries) - log.debug("Sending HTTP Request: %s %s", request.method, request.url) + retries_taken = 0 + for retries_taken in range(max_retries + 1): + options = model_copy(input_options) + options = self._prepare_options(options) - try: - response = self._client.send( - request, - stream=stream or self._should_stream_response_body(request=request), - **kwargs, - ) - except httpx.TimeoutException as err: - log.debug("Encountered httpx.TimeoutException", exc_info=True) + remaining_retries = max_retries - retries_taken + request = self._build_request(options, retries_taken=retries_taken) + self._prepare_request(request) - if remaining_retries > 0: - return self._retry_request( - input_options, - cast_to, - retries_taken=retries_taken, - stream=stream, - stream_cls=stream_cls, - response_headers=None, - ) + kwargs: HttpxSendArgs = {} + if self.custom_auth is not None: + kwargs["auth"] = self.custom_auth - log.debug("Raising timeout error") - raise APITimeoutError(request=request) from err - except Exception as err: - log.debug("Encountered Exception", exc_info=True) + log.debug("Sending HTTP Request: %s %s", request.method, request.url) - if remaining_retries > 0: - return self._retry_request( - input_options, - cast_to, - retries_taken=retries_taken, - stream=stream, - stream_cls=stream_cls, - response_headers=None, + response = None + try: + response = self._client.send( + request, + stream=stream or self._should_stream_response_body(request=request), + **kwargs, ) + except httpx.TimeoutException as err: + log.debug("Encountered httpx.TimeoutException", exc_info=True) + + if remaining_retries > 0: + self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=None, + ) + continue + + log.debug("Raising timeout error") + raise APITimeoutError(request=request) from err + except Exception as err: + log.debug("Encountered Exception", exc_info=True) + + if remaining_retries > 0: + self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=None, + ) + continue + + log.debug("Raising connection error") + raise APIConnectionError(request=request) from err + + log.debug( + 'HTTP Response: %s %s "%i %s" %s', + request.method, + request.url, + response.status_code, + response.reason_phrase, + response.headers, + ) - log.debug("Raising connection error") - raise APIConnectionError(request=request) from err - - log.debug( - 'HTTP Response: %s %s "%i %s" %s', - request.method, - request.url, - response.status_code, - response.reason_phrase, - response.headers, - ) + try: + response.raise_for_status() + except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code + log.debug("Encountered httpx.HTTPStatusError", exc_info=True) + + if remaining_retries > 0 and self._should_retry(err.response): + err.response.close() + self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=response, + ) + continue - try: - response.raise_for_status() - except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code - log.debug("Encountered httpx.HTTPStatusError", exc_info=True) - - if remaining_retries > 0 and self._should_retry(err.response): - err.response.close() - return self._retry_request( - input_options, - cast_to, - retries_taken=retries_taken, - response_headers=err.response.headers, - stream=stream, - stream_cls=stream_cls, - ) + # If the response is streamed then we need to explicitly read the response + # to completion before attempting to access the response text. + if not err.response.is_closed: + err.response.read() - # If the response is streamed then we need to explicitly read the response - # to completion before attempting to access the response text. - if not err.response.is_closed: - err.response.read() + log.debug("Re-raising status error") + raise self._make_status_error_from_response(err.response) from None - log.debug("Re-raising status error") - raise self._make_status_error_from_response(err.response) from None + break + assert response is not None, "could not resolve response (should never happen)" return self._process_response( cast_to=cast_to, options=options, @@ -1062,37 +1042,20 @@ def _request( retries_taken=retries_taken, ) - def _retry_request( - self, - options: FinalRequestOptions, - cast_to: Type[ResponseT], - *, - retries_taken: int, - response_headers: httpx.Headers | None, - stream: bool, - stream_cls: type[_StreamT] | None, - ) -> ResponseT | _StreamT: - remaining_retries = options.get_max_retries(self.max_retries) - retries_taken + def _sleep_for_retry( + self, *, retries_taken: int, max_retries: int, options: FinalRequestOptions, response: httpx.Response | None + ) -> None: + remaining_retries = max_retries - retries_taken if remaining_retries == 1: log.debug("1 retry left") else: log.debug("%i retries left", remaining_retries) - timeout = self._calculate_retry_timeout(remaining_retries, options, response_headers) + timeout = self._calculate_retry_timeout(remaining_retries, options, response.headers if response else None) log.info("Retrying request to %s in %f seconds", options.url, timeout) - # In a synchronous context we are blocking the entire thread. Up to the library user to run the client in a - # different thread if necessary. time.sleep(timeout) - return self._request( - options=options, - cast_to=cast_to, - retries_taken=retries_taken + 1, - stream=stream, - stream_cls=stream_cls, - ) - def _process_response( self, *, @@ -1436,7 +1399,6 @@ async def request( options: FinalRequestOptions, *, stream: Literal[False] = False, - remaining_retries: Optional[int] = None, ) -> ResponseT: ... @overload @@ -1447,7 +1409,6 @@ async def request( *, stream: Literal[True], stream_cls: type[_AsyncStreamT], - remaining_retries: Optional[int] = None, ) -> _AsyncStreamT: ... @overload @@ -1458,7 +1419,6 @@ async def request( *, stream: bool, stream_cls: type[_AsyncStreamT] | None = None, - remaining_retries: Optional[int] = None, ) -> ResponseT | _AsyncStreamT: ... async def request( @@ -1468,120 +1428,111 @@ async def request( *, stream: bool = False, stream_cls: type[_AsyncStreamT] | None = None, - remaining_retries: Optional[int] = None, - ) -> ResponseT | _AsyncStreamT: - if remaining_retries is not None: - retries_taken = options.get_max_retries(self.max_retries) - remaining_retries - else: - retries_taken = 0 - - return await self._request( - cast_to=cast_to, - options=options, - stream=stream, - stream_cls=stream_cls, - retries_taken=retries_taken, - ) - - async def _request( - self, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - *, - stream: bool, - stream_cls: type[_AsyncStreamT] | None, - retries_taken: int, ) -> ResponseT | _AsyncStreamT: if self._platform is None: # `get_platform` can make blocking IO calls so we # execute it earlier while we are in an async context self._platform = await asyncify(get_platform)() + cast_to = self._maybe_override_cast_to(cast_to, options) + # create a copy of the options we were given so that if the # options are mutated later & we then retry, the retries are # given the original options input_options = model_copy(options) - - cast_to = self._maybe_override_cast_to(cast_to, options) - options = await self._prepare_options(options) - - remaining_retries = options.get_max_retries(self.max_retries) - retries_taken - request = self._build_request(options, retries_taken=retries_taken) - await self._prepare_request(request) - - if options.idempotency_key: + if input_options.idempotency_key is None and input_options.method.lower() != "get": # ensure the idempotency key is reused between requests - input_options.idempotency_key = options.idempotency_key + input_options.idempotency_key = self._idempotency_key() - kwargs: HttpxSendArgs = {} - if self.custom_auth is not None: - kwargs["auth"] = self.custom_auth + response: httpx.Response | None = None + max_retries = input_options.get_max_retries(self.max_retries) - try: - response = await self._client.send( - request, - stream=stream or self._should_stream_response_body(request=request), - **kwargs, - ) - except httpx.TimeoutException as err: - log.debug("Encountered httpx.TimeoutException", exc_info=True) + retries_taken = 0 + for retries_taken in range(max_retries + 1): + options = model_copy(input_options) + options = await self._prepare_options(options) - if remaining_retries > 0: - return await self._retry_request( - input_options, - cast_to, - retries_taken=retries_taken, - stream=stream, - stream_cls=stream_cls, - response_headers=None, - ) + remaining_retries = max_retries - retries_taken + request = self._build_request(options, retries_taken=retries_taken) + await self._prepare_request(request) - log.debug("Raising timeout error") - raise APITimeoutError(request=request) from err - except Exception as err: - log.debug("Encountered Exception", exc_info=True) + kwargs: HttpxSendArgs = {} + if self.custom_auth is not None: + kwargs["auth"] = self.custom_auth - if remaining_retries > 0: - return await self._retry_request( - input_options, - cast_to, - retries_taken=retries_taken, - stream=stream, - stream_cls=stream_cls, - response_headers=None, - ) + log.debug("Sending HTTP Request: %s %s", request.method, request.url) - log.debug("Raising connection error") - raise APIConnectionError(request=request) from err + response = None + try: + response = await self._client.send( + request, + stream=stream or self._should_stream_response_body(request=request), + **kwargs, + ) + except httpx.TimeoutException as err: + log.debug("Encountered httpx.TimeoutException", exc_info=True) + + if remaining_retries > 0: + await self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=None, + ) + continue + + log.debug("Raising timeout error") + raise APITimeoutError(request=request) from err + except Exception as err: + log.debug("Encountered Exception", exc_info=True) + + if remaining_retries > 0: + await self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=None, + ) + continue + + log.debug("Raising connection error") + raise APIConnectionError(request=request) from err + + log.debug( + 'HTTP Response: %s %s "%i %s" %s', + request.method, + request.url, + response.status_code, + response.reason_phrase, + response.headers, + ) - log.debug( - 'HTTP Request: %s %s "%i %s"', request.method, request.url, response.status_code, response.reason_phrase - ) + try: + response.raise_for_status() + except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code + log.debug("Encountered httpx.HTTPStatusError", exc_info=True) + + if remaining_retries > 0 and self._should_retry(err.response): + await err.response.aclose() + await self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=response, + ) + continue - try: - response.raise_for_status() - except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code - log.debug("Encountered httpx.HTTPStatusError", exc_info=True) - - if remaining_retries > 0 and self._should_retry(err.response): - await err.response.aclose() - return await self._retry_request( - input_options, - cast_to, - retries_taken=retries_taken, - response_headers=err.response.headers, - stream=stream, - stream_cls=stream_cls, - ) + # If the response is streamed then we need to explicitly read the response + # to completion before attempting to access the response text. + if not err.response.is_closed: + await err.response.aread() - # If the response is streamed then we need to explicitly read the response - # to completion before attempting to access the response text. - if not err.response.is_closed: - await err.response.aread() + log.debug("Re-raising status error") + raise self._make_status_error_from_response(err.response) from None - log.debug("Re-raising status error") - raise self._make_status_error_from_response(err.response) from None + break + assert response is not None, "could not resolve response (should never happen)" return await self._process_response( cast_to=cast_to, options=options, @@ -1591,35 +1542,20 @@ async def _request( retries_taken=retries_taken, ) - async def _retry_request( - self, - options: FinalRequestOptions, - cast_to: Type[ResponseT], - *, - retries_taken: int, - response_headers: httpx.Headers | None, - stream: bool, - stream_cls: type[_AsyncStreamT] | None, - ) -> ResponseT | _AsyncStreamT: - remaining_retries = options.get_max_retries(self.max_retries) - retries_taken + async def _sleep_for_retry( + self, *, retries_taken: int, max_retries: int, options: FinalRequestOptions, response: httpx.Response | None + ) -> None: + remaining_retries = max_retries - retries_taken if remaining_retries == 1: log.debug("1 retry left") else: log.debug("%i retries left", remaining_retries) - timeout = self._calculate_retry_timeout(remaining_retries, options, response_headers) + timeout = self._calculate_retry_timeout(remaining_retries, options, response.headers if response else None) log.info("Retrying request to %s in %f seconds", options.url, timeout) await anyio.sleep(timeout) - return await self._request( - options=options, - cast_to=cast_to, - retries_taken=retries_taken + 1, - stream=stream, - stream_cls=stream_cls, - ) - async def _process_response( self, *, From 682f143ba3d7ef7e11e3f896b36c7049ad9adb87 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 22 Apr 2025 22:59:29 +0000 Subject: [PATCH 238/399] docs(api): updates to API spec --- .stats.yml | 4 +- README.md | 9 +- src/writerai/resources/chat.py | 86 ++++++++++++++++--- src/writerai/resources/completions.py | 34 +++++--- src/writerai/types/chat_chat_params.py | 26 +++++- .../types/completion_create_params.py | 7 +- tests/api_resources/test_chat.py | 48 +++++++---- tests/test_client.py | 12 +-- 8 files changed, 163 insertions(+), 63 deletions(-) diff --git a/.stats.yml b/.stats.yml index 5e11c3db..8cc59f95 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 31 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-8de5485a6892144049ab388a3dc5378c3319cbbe551c3db1e512bb620ab414ec.yml -openapi_spec_hash: c93452a50c9ef635a5fae65830c3cc81 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-a0e83ebfd81856b538d16c7a41ccdc11687ee558e1435a40f3bb7e0d6e1ab7c8.yml +openapi_spec_hash: 8ac62303a9158c13f344975cb0786bc6 config_hash: 1f6d0bf7309d0007e28ab85b89a0de85 diff --git a/README.md b/README.md index d44a0163..2a343f7f 100644 --- a/README.md +++ b/README.md @@ -207,10 +207,13 @@ client = Writer() chat_completion = client.chat.chat( messages=[{"role": "user"}], - model="palmyra-x-004", - stream_options={"include_usage": True}, + model="model", + response_format={ + "type": "text", + "json_schema": {}, + }, ) -print(chat_completion.stream_options) +print(chat_completion.response_format) ``` ## Handling errors diff --git a/src/writerai/resources/chat.py b/src/writerai/resources/chat.py index 0cce886a..324b1902 100644 --- a/src/writerai/resources/chat.py +++ b/src/writerai/resources/chat.py @@ -52,10 +52,11 @@ def chat( self, *, messages: Iterable[chat_chat_params.Message], - model: Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"], + model: str, logprobs: bool | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, + response_format: chat_chat_params.ResponseFormat | NotGiven = NOT_GIVEN, stop: Union[List[str], str] | NotGiven = NOT_GIVEN, stream: Literal[False] | NotGiven = NOT_GIVEN, stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, @@ -81,7 +82,8 @@ def chat( the model to respond to. The array must contain at least one message. model: The [ID of the model](https://dev.writer.com/home/models) to use for creating - the chat completion. + the chat completion. Supports `palmyra-x-004`, `palmyra-fin`, `palmyra-med`, + `palmyra-creative`, and `palmyra-x-003-instruct`. logprobs: Specifies whether to return log probabilities of the output tokens. @@ -93,6 +95,13 @@ def chat( single request. This parameter allows for generating multiple responses, offering a variety of potential replies from which to choose. + response_format: The response format to use for the chat completion, available with + `palmyra-x-004`. + + `text` is the default response format. [JSON Schema](https://json-schema.org/) + is supported for structured responses. If you specify `json_schema`, you must + also provide a `json_schema` object. + stop: A token or sequence of tokens that, when generated, will cause the model to stop producing further content. This can be a single token or an array of tokens, acting as a signal to end the output. @@ -139,11 +148,12 @@ def chat( self, *, messages: Iterable[chat_chat_params.Message], - model: Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"], + model: str, stream: Literal[True], logprobs: bool | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, + response_format: chat_chat_params.ResponseFormat | NotGiven = NOT_GIVEN, stop: Union[List[str], str] | NotGiven = NOT_GIVEN, stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, @@ -168,7 +178,8 @@ def chat( the model to respond to. The array must contain at least one message. model: The [ID of the model](https://dev.writer.com/home/models) to use for creating - the chat completion. + the chat completion. Supports `palmyra-x-004`, `palmyra-fin`, `palmyra-med`, + `palmyra-creative`, and `palmyra-x-003-instruct`. stream: Indicates whether the response should be streamed incrementally as it is generated or only returned once fully complete. Streaming can be useful for @@ -184,6 +195,13 @@ def chat( single request. This parameter allows for generating multiple responses, offering a variety of potential replies from which to choose. + response_format: The response format to use for the chat completion, available with + `palmyra-x-004`. + + `text` is the default response format. [JSON Schema](https://json-schema.org/) + is supported for structured responses. If you specify `json_schema`, you must + also provide a `json_schema` object. + stop: A token or sequence of tokens that, when generated, will cause the model to stop producing further content. This can be a single token or an array of tokens, acting as a signal to end the output. @@ -226,11 +244,12 @@ def chat( self, *, messages: Iterable[chat_chat_params.Message], - model: Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"], + model: str, stream: bool, logprobs: bool | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, + response_format: chat_chat_params.ResponseFormat | NotGiven = NOT_GIVEN, stop: Union[List[str], str] | NotGiven = NOT_GIVEN, stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, @@ -255,7 +274,8 @@ def chat( the model to respond to. The array must contain at least one message. model: The [ID of the model](https://dev.writer.com/home/models) to use for creating - the chat completion. + the chat completion. Supports `palmyra-x-004`, `palmyra-fin`, `palmyra-med`, + `palmyra-creative`, and `palmyra-x-003-instruct`. stream: Indicates whether the response should be streamed incrementally as it is generated or only returned once fully complete. Streaming can be useful for @@ -271,6 +291,13 @@ def chat( single request. This parameter allows for generating multiple responses, offering a variety of potential replies from which to choose. + response_format: The response format to use for the chat completion, available with + `palmyra-x-004`. + + `text` is the default response format. [JSON Schema](https://json-schema.org/) + is supported for structured responses. If you specify `json_schema`, you must + also provide a `json_schema` object. + stop: A token or sequence of tokens that, when generated, will cause the model to stop producing further content. This can be a single token or an array of tokens, acting as a signal to end the output. @@ -313,10 +340,11 @@ def chat( self, *, messages: Iterable[chat_chat_params.Message], - model: Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"], + model: str, logprobs: bool | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, + response_format: chat_chat_params.ResponseFormat | NotGiven = NOT_GIVEN, stop: Union[List[str], str] | NotGiven = NOT_GIVEN, stream: Literal[False] | Literal[True] | NotGiven = NOT_GIVEN, stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, @@ -340,6 +368,7 @@ def chat( "logprobs": logprobs, "max_tokens": max_tokens, "n": n, + "response_format": response_format, "stop": stop, "stream": stream, "stream_options": stream_options, @@ -384,10 +413,11 @@ async def chat( self, *, messages: Iterable[chat_chat_params.Message], - model: Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"], + model: str, logprobs: bool | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, + response_format: chat_chat_params.ResponseFormat | NotGiven = NOT_GIVEN, stop: Union[List[str], str] | NotGiven = NOT_GIVEN, stream: Literal[False] | NotGiven = NOT_GIVEN, stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, @@ -413,7 +443,8 @@ async def chat( the model to respond to. The array must contain at least one message. model: The [ID of the model](https://dev.writer.com/home/models) to use for creating - the chat completion. + the chat completion. Supports `palmyra-x-004`, `palmyra-fin`, `palmyra-med`, + `palmyra-creative`, and `palmyra-x-003-instruct`. logprobs: Specifies whether to return log probabilities of the output tokens. @@ -425,6 +456,13 @@ async def chat( single request. This parameter allows for generating multiple responses, offering a variety of potential replies from which to choose. + response_format: The response format to use for the chat completion, available with + `palmyra-x-004`. + + `text` is the default response format. [JSON Schema](https://json-schema.org/) + is supported for structured responses. If you specify `json_schema`, you must + also provide a `json_schema` object. + stop: A token or sequence of tokens that, when generated, will cause the model to stop producing further content. This can be a single token or an array of tokens, acting as a signal to end the output. @@ -471,11 +509,12 @@ async def chat( self, *, messages: Iterable[chat_chat_params.Message], - model: Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"], + model: str, stream: Literal[True], logprobs: bool | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, + response_format: chat_chat_params.ResponseFormat | NotGiven = NOT_GIVEN, stop: Union[List[str], str] | NotGiven = NOT_GIVEN, stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, @@ -500,7 +539,8 @@ async def chat( the model to respond to. The array must contain at least one message. model: The [ID of the model](https://dev.writer.com/home/models) to use for creating - the chat completion. + the chat completion. Supports `palmyra-x-004`, `palmyra-fin`, `palmyra-med`, + `palmyra-creative`, and `palmyra-x-003-instruct`. stream: Indicates whether the response should be streamed incrementally as it is generated or only returned once fully complete. Streaming can be useful for @@ -516,6 +556,13 @@ async def chat( single request. This parameter allows for generating multiple responses, offering a variety of potential replies from which to choose. + response_format: The response format to use for the chat completion, available with + `palmyra-x-004`. + + `text` is the default response format. [JSON Schema](https://json-schema.org/) + is supported for structured responses. If you specify `json_schema`, you must + also provide a `json_schema` object. + stop: A token or sequence of tokens that, when generated, will cause the model to stop producing further content. This can be a single token or an array of tokens, acting as a signal to end the output. @@ -558,11 +605,12 @@ async def chat( self, *, messages: Iterable[chat_chat_params.Message], - model: Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"], + model: str, stream: bool, logprobs: bool | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, + response_format: chat_chat_params.ResponseFormat | NotGiven = NOT_GIVEN, stop: Union[List[str], str] | NotGiven = NOT_GIVEN, stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, @@ -587,7 +635,8 @@ async def chat( the model to respond to. The array must contain at least one message. model: The [ID of the model](https://dev.writer.com/home/models) to use for creating - the chat completion. + the chat completion. Supports `palmyra-x-004`, `palmyra-fin`, `palmyra-med`, + `palmyra-creative`, and `palmyra-x-003-instruct`. stream: Indicates whether the response should be streamed incrementally as it is generated or only returned once fully complete. Streaming can be useful for @@ -603,6 +652,13 @@ async def chat( single request. This parameter allows for generating multiple responses, offering a variety of potential replies from which to choose. + response_format: The response format to use for the chat completion, available with + `palmyra-x-004`. + + `text` is the default response format. [JSON Schema](https://json-schema.org/) + is supported for structured responses. If you specify `json_schema`, you must + also provide a `json_schema` object. + stop: A token or sequence of tokens that, when generated, will cause the model to stop producing further content. This can be a single token or an array of tokens, acting as a signal to end the output. @@ -645,10 +701,11 @@ async def chat( self, *, messages: Iterable[chat_chat_params.Message], - model: Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"], + model: str, logprobs: bool | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, + response_format: chat_chat_params.ResponseFormat | NotGiven = NOT_GIVEN, stop: Union[List[str], str] | NotGiven = NOT_GIVEN, stream: Literal[False] | Literal[True] | NotGiven = NOT_GIVEN, stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, @@ -672,6 +729,7 @@ async def chat( "logprobs": logprobs, "max_tokens": max_tokens, "n": n, + "response_format": response_format, "stop": stop, "stream": stream, "stream_options": stream_options, diff --git a/src/writerai/resources/completions.py b/src/writerai/resources/completions.py index 87141ed0..5666d30a 100644 --- a/src/writerai/resources/completions.py +++ b/src/writerai/resources/completions.py @@ -50,7 +50,7 @@ def with_streaming_response(self) -> CompletionsResourceWithStreamingResponse: def create( self, *, - model: Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"], + model: str, prompt: str, best_of: int | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, @@ -71,7 +71,8 @@ def create( Args: model: The [ID of the model](https://dev.writer.com/home/models) to use for generating - text. + text. Supports `palmyra-x-004`, `palmyra-fin`, `palmyra-med`, + `palmyra-creative`, and `palmyra-x-003-instruct`. prompt: The input text that the model will process to generate a response. @@ -112,7 +113,7 @@ def create( def create( self, *, - model: Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"], + model: str, prompt: str, stream: Literal[True], best_of: int | NotGiven = NOT_GIVEN, @@ -133,7 +134,8 @@ def create( Args: model: The [ID of the model](https://dev.writer.com/home/models) to use for generating - text. + text. Supports `palmyra-x-004`, `palmyra-fin`, `palmyra-med`, + `palmyra-creative`, and `palmyra-x-003-instruct`. prompt: The input text that the model will process to generate a response. @@ -174,7 +176,7 @@ def create( def create( self, *, - model: Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"], + model: str, prompt: str, stream: bool, best_of: int | NotGiven = NOT_GIVEN, @@ -195,7 +197,8 @@ def create( Args: model: The [ID of the model](https://dev.writer.com/home/models) to use for generating - text. + text. Supports `palmyra-x-004`, `palmyra-fin`, `palmyra-med`, + `palmyra-creative`, and `palmyra-x-003-instruct`. prompt: The input text that the model will process to generate a response. @@ -236,7 +239,7 @@ def create( def create( self, *, - model: Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"], + model: str, prompt: str, best_of: int | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, @@ -303,7 +306,7 @@ def with_streaming_response(self) -> AsyncCompletionsResourceWithStreamingRespon async def create( self, *, - model: Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"], + model: str, prompt: str, best_of: int | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, @@ -324,7 +327,8 @@ async def create( Args: model: The [ID of the model](https://dev.writer.com/home/models) to use for generating - text. + text. Supports `palmyra-x-004`, `palmyra-fin`, `palmyra-med`, + `palmyra-creative`, and `palmyra-x-003-instruct`. prompt: The input text that the model will process to generate a response. @@ -365,7 +369,7 @@ async def create( async def create( self, *, - model: Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"], + model: str, prompt: str, stream: Literal[True], best_of: int | NotGiven = NOT_GIVEN, @@ -386,7 +390,8 @@ async def create( Args: model: The [ID of the model](https://dev.writer.com/home/models) to use for generating - text. + text. Supports `palmyra-x-004`, `palmyra-fin`, `palmyra-med`, + `palmyra-creative`, and `palmyra-x-003-instruct`. prompt: The input text that the model will process to generate a response. @@ -427,7 +432,7 @@ async def create( async def create( self, *, - model: Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"], + model: str, prompt: str, stream: bool, best_of: int | NotGiven = NOT_GIVEN, @@ -448,7 +453,8 @@ async def create( Args: model: The [ID of the model](https://dev.writer.com/home/models) to use for generating - text. + text. Supports `palmyra-x-004`, `palmyra-fin`, `palmyra-med`, + `palmyra-creative`, and `palmyra-x-003-instruct`. prompt: The input text that the model will process to generate a response. @@ -489,7 +495,7 @@ async def create( async def create( self, *, - model: Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"], + model: str, prompt: str, best_of: int | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, diff --git a/src/writerai/types/chat_chat_params.py b/src/writerai/types/chat_chat_params.py index 848e8489..dfc945e4 100644 --- a/src/writerai/types/chat_chat_params.py +++ b/src/writerai/types/chat_chat_params.py @@ -14,6 +14,7 @@ __all__ = [ "ChatChatParamsBase", "Message", + "ResponseFormat", "StreamOptions", "ToolChoice", "ChatChatParamsNonStreaming", @@ -28,12 +29,11 @@ class ChatChatParamsBase(TypedDict, total=False): the model to respond to. The array must contain at least one message. """ - model: Required[ - Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"] - ] + model: Required[str] """ The [ID of the model](https://dev.writer.com/home/models) to use for creating - the chat completion. + the chat completion. Supports `palmyra-x-004`, `palmyra-fin`, `palmyra-med`, + `palmyra-creative`, and `palmyra-x-003-instruct`. """ logprobs: bool @@ -53,6 +53,16 @@ class ChatChatParamsBase(TypedDict, total=False): offering a variety of potential replies from which to choose. """ + response_format: ResponseFormat + """ + The response format to use for the chat completion, available with + `palmyra-x-004`. + + `text` is the default response format. [JSON Schema](https://json-schema.org/) + is supported for structured responses. If you specify `json_schema`, you must + also provide a `json_schema` object. + """ + stop: Union[List[str], str] """ A token or sequence of tokens that, when generated, will cause the model to stop @@ -120,6 +130,14 @@ class Message(TypedDict, total=False): tool_calls: Optional[Iterable[ToolCall]] +class ResponseFormat(TypedDict, total=False): + type: Required[Literal["text", "json_schema"]] + """The type of response format to use.""" + + json_schema: object + """The JSON schema to use for the response format.""" + + class StreamOptions(TypedDict, total=False): include_usage: Required[bool] """Indicate whether to include usage information.""" diff --git a/src/writerai/types/completion_create_params.py b/src/writerai/types/completion_create_params.py index 0b926268..e55ffba5 100644 --- a/src/writerai/types/completion_create_params.py +++ b/src/writerai/types/completion_create_params.py @@ -9,12 +9,11 @@ class CompletionCreateParamsBase(TypedDict, total=False): - model: Required[ - Literal["palmyra-x-004", "palmyra-fin", "palmyra-med", "palmyra-creative", "palmyra-x-003-instruct"] - ] + model: Required[str] """ The [ID of the model](https://dev.writer.com/home/models) to use for generating - text. + text. Supports `palmyra-x-004`, `palmyra-fin`, `palmyra-med`, + `palmyra-creative`, and `palmyra-x-003-instruct`. """ prompt: Required[str] diff --git a/tests/api_resources/test_chat.py b/tests/api_resources/test_chat.py index 9269f03a..d04446a9 100644 --- a/tests/api_resources/test_chat.py +++ b/tests/api_resources/test_chat.py @@ -21,7 +21,7 @@ class TestChat: def test_method_chat_overload_1(self, client: Writer) -> None: chat = client.chat.chat( messages=[{"role": "user"}], - model="palmyra-x-004", + model="model", ) assert_matches_type(ChatCompletion, chat, path=["response"]) @@ -69,10 +69,14 @@ def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: ], } ], - model="palmyra-x-004", + model="model", logprobs=True, max_tokens=0, n=0, + response_format={ + "type": "text", + "json_schema": {}, + }, stop=["string"], stream=False, stream_options={"include_usage": True}, @@ -96,7 +100,7 @@ def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: def test_raw_response_chat_overload_1(self, client: Writer) -> None: response = client.chat.with_raw_response.chat( messages=[{"role": "user"}], - model="palmyra-x-004", + model="model", ) assert response.is_closed is True @@ -108,7 +112,7 @@ def test_raw_response_chat_overload_1(self, client: Writer) -> None: def test_streaming_response_chat_overload_1(self, client: Writer) -> None: with client.chat.with_streaming_response.chat( messages=[{"role": "user"}], - model="palmyra-x-004", + model="model", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -122,7 +126,7 @@ def test_streaming_response_chat_overload_1(self, client: Writer) -> None: def test_method_chat_overload_2(self, client: Writer) -> None: chat_stream = client.chat.chat( messages=[{"role": "user"}], - model="palmyra-x-004", + model="model", stream=True, ) chat_stream.response.close() @@ -171,11 +175,15 @@ def test_method_chat_with_all_params_overload_2(self, client: Writer) -> None: ], } ], - model="palmyra-x-004", + model="model", stream=True, logprobs=True, max_tokens=0, n=0, + response_format={ + "type": "text", + "json_schema": {}, + }, stop=["string"], stream_options={"include_usage": True}, temperature=0, @@ -198,7 +206,7 @@ def test_method_chat_with_all_params_overload_2(self, client: Writer) -> None: def test_raw_response_chat_overload_2(self, client: Writer) -> None: response = client.chat.with_raw_response.chat( messages=[{"role": "user"}], - model="palmyra-x-004", + model="model", stream=True, ) @@ -210,7 +218,7 @@ def test_raw_response_chat_overload_2(self, client: Writer) -> None: def test_streaming_response_chat_overload_2(self, client: Writer) -> None: with client.chat.with_streaming_response.chat( messages=[{"role": "user"}], - model="palmyra-x-004", + model="model", stream=True, ) as response: assert not response.is_closed @@ -229,7 +237,7 @@ class TestAsyncChat: async def test_method_chat_overload_1(self, async_client: AsyncWriter) -> None: chat = await async_client.chat.chat( messages=[{"role": "user"}], - model="palmyra-x-004", + model="model", ) assert_matches_type(ChatCompletion, chat, path=["response"]) @@ -277,10 +285,14 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW ], } ], - model="palmyra-x-004", + model="model", logprobs=True, max_tokens=0, n=0, + response_format={ + "type": "text", + "json_schema": {}, + }, stop=["string"], stream=False, stream_options={"include_usage": True}, @@ -304,7 +316,7 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW async def test_raw_response_chat_overload_1(self, async_client: AsyncWriter) -> None: response = await async_client.chat.with_raw_response.chat( messages=[{"role": "user"}], - model="palmyra-x-004", + model="model", ) assert response.is_closed is True @@ -316,7 +328,7 @@ async def test_raw_response_chat_overload_1(self, async_client: AsyncWriter) -> async def test_streaming_response_chat_overload_1(self, async_client: AsyncWriter) -> None: async with async_client.chat.with_streaming_response.chat( messages=[{"role": "user"}], - model="palmyra-x-004", + model="model", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -330,7 +342,7 @@ async def test_streaming_response_chat_overload_1(self, async_client: AsyncWrite async def test_method_chat_overload_2(self, async_client: AsyncWriter) -> None: chat_stream = await async_client.chat.chat( messages=[{"role": "user"}], - model="palmyra-x-004", + model="model", stream=True, ) await chat_stream.response.aclose() @@ -379,11 +391,15 @@ async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncW ], } ], - model="palmyra-x-004", + model="model", stream=True, logprobs=True, max_tokens=0, n=0, + response_format={ + "type": "text", + "json_schema": {}, + }, stop=["string"], stream_options={"include_usage": True}, temperature=0, @@ -406,7 +422,7 @@ async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncW async def test_raw_response_chat_overload_2(self, async_client: AsyncWriter) -> None: response = await async_client.chat.with_raw_response.chat( messages=[{"role": "user"}], - model="palmyra-x-004", + model="model", stream=True, ) @@ -418,7 +434,7 @@ async def test_raw_response_chat_overload_2(self, async_client: AsyncWriter) -> async def test_streaming_response_chat_overload_2(self, async_client: AsyncWriter) -> None: async with async_client.chat.with_streaming_response.chat( messages=[{"role": "user"}], - model="palmyra-x-004", + model="model", stream=True, ) as response: assert not response.is_closed diff --git a/tests/test_client.py b/tests/test_client.py index 265babef..c43a200d 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -805,7 +805,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) - response = client.chat.with_raw_response.chat(messages=[{"role": "user"}], model="palmyra-x-004") + response = client.chat.with_raw_response.chat(messages=[{"role": "user"}], model="model") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -830,7 +830,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) response = client.chat.with_raw_response.chat( - messages=[{"role": "user"}], model="palmyra-x-004", extra_headers={"x-stainless-retry-count": Omit()} + messages=[{"role": "user"}], model="model", extra_headers={"x-stainless-retry-count": Omit()} ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -855,7 +855,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) response = client.chat.with_raw_response.chat( - messages=[{"role": "user"}], model="palmyra-x-004", extra_headers={"x-stainless-retry-count": "42"} + messages=[{"role": "user"}], model="model", extra_headers={"x-stainless-retry-count": "42"} ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" @@ -1626,7 +1626,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) - response = await client.chat.with_raw_response.chat(messages=[{"role": "user"}], model="palmyra-x-004") + response = await client.chat.with_raw_response.chat(messages=[{"role": "user"}], model="model") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -1652,7 +1652,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) response = await client.chat.with_raw_response.chat( - messages=[{"role": "user"}], model="palmyra-x-004", extra_headers={"x-stainless-retry-count": Omit()} + messages=[{"role": "user"}], model="model", extra_headers={"x-stainless-retry-count": Omit()} ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -1678,7 +1678,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v1/chat").mock(side_effect=retry_handler) response = await client.chat.with_raw_response.chat( - messages=[{"role": "user"}], model="palmyra-x-004", extra_headers={"x-stainless-retry-count": "42"} + messages=[{"role": "user"}], model="model", extra_headers={"x-stainless-retry-count": "42"} ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" From 54c05786ff79ec56ccab7a40f0e9eb3fd5ab461c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 22 Apr 2025 23:28:47 +0000 Subject: [PATCH 239/399] fix(pydantic v1): more robust ModelField.annotation check --- src/writerai/_models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/writerai/_models.py b/src/writerai/_models.py index 58b9263e..798956f1 100644 --- a/src/writerai/_models.py +++ b/src/writerai/_models.py @@ -626,8 +626,8 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, # Note: if one variant defines an alias then they all should discriminator_alias = field_info.alias - if field_info.annotation and is_literal_type(field_info.annotation): - for entry in get_args(field_info.annotation): + if (annotation := getattr(field_info, "annotation", None)) and is_literal_type(annotation): + for entry in get_args(annotation): if isinstance(entry, str): mapping[entry] = variant From 9d8dc47cf4cb991c93e6757b404814cb1650867a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 23 Apr 2025 13:08:30 +0000 Subject: [PATCH 240/399] feat(api): add ai detect tool endpoint --- .stats.yml | 4 +- api.md | 7 +- src/writerai/resources/tools/tools.py | 89 ++++++++++++++++++- src/writerai/types/__init__.py | 2 + src/writerai/types/tool_ai_detect_params.py | 15 ++++ src/writerai/types/tool_ai_detect_response.py | 13 +++ tests/api_resources/test_tools.py | 63 +++++++++++++ 7 files changed, 189 insertions(+), 4 deletions(-) create mode 100644 src/writerai/types/tool_ai_detect_params.py create mode 100644 src/writerai/types/tool_ai_detect_response.py diff --git a/.stats.yml b/.stats.yml index 8cc59f95..a6212227 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 31 +configured_endpoints: 32 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-a0e83ebfd81856b538d16c7a41ccdc11687ee558e1435a40f3bb7e0d6e1ab7c8.yml openapi_spec_hash: 8ac62303a9158c13f344975cb0786bc6 -config_hash: 1f6d0bf7309d0007e28ab85b89a0de85 +config_hash: b1d3b26784527ee46af49c1bb9e93193 diff --git a/api.md b/api.md index 8a83ff66..5ebeffe3 100644 --- a/api.md +++ b/api.md @@ -162,11 +162,16 @@ Methods: Types: ```python -from writerai.types import ToolContextAwareSplittingResponse, ToolParsePdfResponse +from writerai.types import ( + ToolAIDetectResponse, + ToolContextAwareSplittingResponse, + ToolParsePdfResponse, +) ``` Methods: +- client.tools.ai_detect(\*\*params) -> ToolAIDetectResponse - client.tools.context_aware_splitting(\*\*params) -> ToolContextAwareSplittingResponse - client.tools.parse_pdf(file_id, \*\*params) -> ToolParsePdfResponse diff --git a/src/writerai/resources/tools/tools.py b/src/writerai/resources/tools/tools.py index ad2d6c1e..132ea276 100644 --- a/src/writerai/resources/tools/tools.py +++ b/src/writerai/resources/tools/tools.py @@ -6,7 +6,7 @@ import httpx -from ...types import tool_parse_pdf_params, tool_context_aware_splitting_params +from ...types import tool_ai_detect_params, tool_parse_pdf_params, tool_context_aware_splitting_params from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven from ..._utils import maybe_transform, async_maybe_transform from ..._compat import cached_property @@ -26,6 +26,7 @@ async_to_streamed_response_wrapper, ) from ..._base_client import make_request_options +from ...types.tool_ai_detect_response import ToolAIDetectResponse from ...types.tool_parse_pdf_response import ToolParsePdfResponse from ...types.tool_context_aware_splitting_response import ToolContextAwareSplittingResponse @@ -56,6 +57,43 @@ def with_streaming_response(self) -> ToolsResourceWithStreamingResponse: """ return ToolsResourceWithStreamingResponse(self) + def ai_detect( + self, + *, + input: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ToolAIDetectResponse: + """Detects if content is AI- or human-generated, with a confidence score. + + Content + must have at least 350 characters + + Args: + input: The content to determine if it is AI- or human-generated. Content must have at + least 350 characters. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/v1/tools/ai-detect", + body=maybe_transform({"input": input}, tool_ai_detect_params.ToolAIDetectParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ToolAIDetectResponse, + ) + def context_aware_splitting( self, *, @@ -164,6 +202,43 @@ def with_streaming_response(self) -> AsyncToolsResourceWithStreamingResponse: """ return AsyncToolsResourceWithStreamingResponse(self) + async def ai_detect( + self, + *, + input: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ToolAIDetectResponse: + """Detects if content is AI- or human-generated, with a confidence score. + + Content + must have at least 350 characters + + Args: + input: The content to determine if it is AI- or human-generated. Content must have at + least 350 characters. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/v1/tools/ai-detect", + body=await async_maybe_transform({"input": input}, tool_ai_detect_params.ToolAIDetectParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ToolAIDetectResponse, + ) + async def context_aware_splitting( self, *, @@ -252,6 +327,9 @@ class ToolsResourceWithRawResponse: def __init__(self, tools: ToolsResource) -> None: self._tools = tools + self.ai_detect = to_raw_response_wrapper( + tools.ai_detect, + ) self.context_aware_splitting = to_raw_response_wrapper( tools.context_aware_splitting, ) @@ -268,6 +346,9 @@ class AsyncToolsResourceWithRawResponse: def __init__(self, tools: AsyncToolsResource) -> None: self._tools = tools + self.ai_detect = async_to_raw_response_wrapper( + tools.ai_detect, + ) self.context_aware_splitting = async_to_raw_response_wrapper( tools.context_aware_splitting, ) @@ -284,6 +365,9 @@ class ToolsResourceWithStreamingResponse: def __init__(self, tools: ToolsResource) -> None: self._tools = tools + self.ai_detect = to_streamed_response_wrapper( + tools.ai_detect, + ) self.context_aware_splitting = to_streamed_response_wrapper( tools.context_aware_splitting, ) @@ -300,6 +384,9 @@ class AsyncToolsResourceWithStreamingResponse: def __init__(self, tools: AsyncToolsResource) -> None: self._tools = tools + self.ai_detect = async_to_streamed_response_wrapper( + tools.ai_detect, + ) self.context_aware_splitting = async_to_streamed_response_wrapper( tools.context_aware_splitting, ) diff --git a/src/writerai/types/__init__.py b/src/writerai/types/__init__.py index aabbde13..16b8f6ef 100644 --- a/src/writerai/types/__init__.py +++ b/src/writerai/types/__init__.py @@ -41,12 +41,14 @@ from .graph_delete_response import GraphDeleteResponse as GraphDeleteResponse from .graph_question_params import GraphQuestionParams as GraphQuestionParams from .graph_update_response import GraphUpdateResponse as GraphUpdateResponse +from .tool_ai_detect_params import ToolAIDetectParams as ToolAIDetectParams from .tool_parse_pdf_params import ToolParsePdfParams as ToolParsePdfParams from .vision_analyze_params import VisionAnalyzeParams as VisionAnalyzeParams from .chat_completion_choice import ChatCompletionChoice as ChatCompletionChoice from .application_list_params import ApplicationListParams as ApplicationListParams from .chat_completion_message import ChatCompletionMessage as ChatCompletionMessage from .question_response_chunk import QuestionResponseChunk as QuestionResponseChunk +from .tool_ai_detect_response import ToolAIDetectResponse as ToolAIDetectResponse from .tool_parse_pdf_response import ToolParsePdfResponse as ToolParsePdfResponse from .completion_create_params import CompletionCreateParams as CompletionCreateParams from .application_list_response import ApplicationListResponse as ApplicationListResponse diff --git a/src/writerai/types/tool_ai_detect_params.py b/src/writerai/types/tool_ai_detect_params.py new file mode 100644 index 00000000..e162d4c3 --- /dev/null +++ b/src/writerai/types/tool_ai_detect_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["ToolAIDetectParams"] + + +class ToolAIDetectParams(TypedDict, total=False): + input: Required[str] + """The content to determine if it is AI- or human-generated. + + Content must have at least 350 characters. + """ diff --git a/src/writerai/types/tool_ai_detect_response.py b/src/writerai/types/tool_ai_detect_response.py new file mode 100644 index 00000000..48052a29 --- /dev/null +++ b/src/writerai/types/tool_ai_detect_response.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["ToolAIDetectResponse"] + + +class ToolAIDetectResponse(BaseModel): + label: Literal["fake", "real"] + + score: float diff --git a/tests/api_resources/test_tools.py b/tests/api_resources/test_tools.py index 0fa0fdc0..47026f82 100644 --- a/tests/api_resources/test_tools.py +++ b/tests/api_resources/test_tools.py @@ -10,6 +10,7 @@ from writerai import Writer, AsyncWriter from tests.utils import assert_matches_type from writerai.types import ( + ToolAIDetectResponse, ToolParsePdfResponse, ToolContextAwareSplittingResponse, ) @@ -20,6 +21,37 @@ class TestTools: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + @parametrize + def test_method_ai_detect(self, client: Writer) -> None: + tool = client.tools.ai_detect( + input="AI and ML continue to be at the forefront of technological advancements. In 2025, we can expect more sophisticated AI systems that can handle complex tasks with greater efficiency. AI will play a crucial role in various sectors, including healthcare, finance, and manufacturing. For instance, AI-powered diagnostic tools will become more accurate, helping doctors detect diseases at an early stage. In finance, AI algorithms will enhance fraud detection and risk management.", + ) + assert_matches_type(ToolAIDetectResponse, tool, path=["response"]) + + @parametrize + def test_raw_response_ai_detect(self, client: Writer) -> None: + response = client.tools.with_raw_response.ai_detect( + input="AI and ML continue to be at the forefront of technological advancements. In 2025, we can expect more sophisticated AI systems that can handle complex tasks with greater efficiency. AI will play a crucial role in various sectors, including healthcare, finance, and manufacturing. For instance, AI-powered diagnostic tools will become more accurate, helping doctors detect diseases at an early stage. In finance, AI algorithms will enhance fraud detection and risk management.", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + tool = response.parse() + assert_matches_type(ToolAIDetectResponse, tool, path=["response"]) + + @parametrize + def test_streaming_response_ai_detect(self, client: Writer) -> None: + with client.tools.with_streaming_response.ai_detect( + input="AI and ML continue to be at the forefront of technological advancements. In 2025, we can expect more sophisticated AI systems that can handle complex tasks with greater efficiency. AI will play a crucial role in various sectors, including healthcare, finance, and manufacturing. For instance, AI-powered diagnostic tools will become more accurate, helping doctors detect diseases at an early stage. In finance, AI algorithms will enhance fraud detection and risk management.", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + tool = response.parse() + assert_matches_type(ToolAIDetectResponse, tool, path=["response"]) + + assert cast(Any, response.is_closed) is True + @parametrize def test_method_context_aware_splitting(self, client: Writer) -> None: tool = client.tools.context_aware_splitting( @@ -100,6 +132,37 @@ def test_path_params_parse_pdf(self, client: Writer) -> None: class TestAsyncTools: parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + @parametrize + async def test_method_ai_detect(self, async_client: AsyncWriter) -> None: + tool = await async_client.tools.ai_detect( + input="AI and ML continue to be at the forefront of technological advancements. In 2025, we can expect more sophisticated AI systems that can handle complex tasks with greater efficiency. AI will play a crucial role in various sectors, including healthcare, finance, and manufacturing. For instance, AI-powered diagnostic tools will become more accurate, helping doctors detect diseases at an early stage. In finance, AI algorithms will enhance fraud detection and risk management.", + ) + assert_matches_type(ToolAIDetectResponse, tool, path=["response"]) + + @parametrize + async def test_raw_response_ai_detect(self, async_client: AsyncWriter) -> None: + response = await async_client.tools.with_raw_response.ai_detect( + input="AI and ML continue to be at the forefront of technological advancements. In 2025, we can expect more sophisticated AI systems that can handle complex tasks with greater efficiency. AI will play a crucial role in various sectors, including healthcare, finance, and manufacturing. For instance, AI-powered diagnostic tools will become more accurate, helping doctors detect diseases at an early stage. In finance, AI algorithms will enhance fraud detection and risk management.", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + tool = await response.parse() + assert_matches_type(ToolAIDetectResponse, tool, path=["response"]) + + @parametrize + async def test_streaming_response_ai_detect(self, async_client: AsyncWriter) -> None: + async with async_client.tools.with_streaming_response.ai_detect( + input="AI and ML continue to be at the forefront of technological advancements. In 2025, we can expect more sophisticated AI systems that can handle complex tasks with greater efficiency. AI will play a crucial role in various sectors, including healthcare, finance, and manufacturing. For instance, AI-powered diagnostic tools will become more accurate, helping doctors detect diseases at an early stage. In finance, AI algorithms will enhance fraud detection and risk management.", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + tool = await response.parse() + assert_matches_type(ToolAIDetectResponse, tool, path=["response"]) + + assert cast(Any, response.is_closed) is True + @parametrize async def test_method_context_aware_splitting(self, async_client: AsyncWriter) -> None: tool = await async_client.tools.context_aware_splitting( From c3cd8eb5fd0c5599ee125f3304127154760f00cf Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 23 Apr 2025 16:21:37 +0000 Subject: [PATCH 241/399] chore(internal): minor formatting changes --- src/writerai/types/completion_chunk.py | 1 - src/writerai/types/file_delete_response.py | 1 - src/writerai/types/graph_delete_response.py | 1 - src/writerai/types/graph_remove_file_from_graph_response.py | 1 - src/writerai/types/question_response_chunk.py | 1 - src/writerai/types/shared/source.py | 1 - src/writerai/types/tool_parse_pdf_response.py | 1 - src/writerai/types/translation_response.py | 1 - src/writerai/types/vision_response.py | 1 - 9 files changed, 9 deletions(-) diff --git a/src/writerai/types/completion_chunk.py b/src/writerai/types/completion_chunk.py index aec58bd3..b92b0a88 100644 --- a/src/writerai/types/completion_chunk.py +++ b/src/writerai/types/completion_chunk.py @@ -1,6 +1,5 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - from .._models import BaseModel __all__ = ["CompletionChunk"] diff --git a/src/writerai/types/file_delete_response.py b/src/writerai/types/file_delete_response.py index 1bea530d..d4735d06 100644 --- a/src/writerai/types/file_delete_response.py +++ b/src/writerai/types/file_delete_response.py @@ -1,6 +1,5 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - from .._models import BaseModel __all__ = ["FileDeleteResponse"] diff --git a/src/writerai/types/graph_delete_response.py b/src/writerai/types/graph_delete_response.py index 40b62701..75b3b640 100644 --- a/src/writerai/types/graph_delete_response.py +++ b/src/writerai/types/graph_delete_response.py @@ -1,6 +1,5 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - from .._models import BaseModel __all__ = ["GraphDeleteResponse"] diff --git a/src/writerai/types/graph_remove_file_from_graph_response.py b/src/writerai/types/graph_remove_file_from_graph_response.py index 83711a69..bde8c696 100644 --- a/src/writerai/types/graph_remove_file_from_graph_response.py +++ b/src/writerai/types/graph_remove_file_from_graph_response.py @@ -1,6 +1,5 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - from .._models import BaseModel __all__ = ["GraphRemoveFileFromGraphResponse"] diff --git a/src/writerai/types/question_response_chunk.py b/src/writerai/types/question_response_chunk.py index 147a00f3..d140acc3 100644 --- a/src/writerai/types/question_response_chunk.py +++ b/src/writerai/types/question_response_chunk.py @@ -1,6 +1,5 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - from .._models import BaseModel from .question import Question diff --git a/src/writerai/types/shared/source.py b/src/writerai/types/shared/source.py index f737aa17..36ae2008 100644 --- a/src/writerai/types/shared/source.py +++ b/src/writerai/types/shared/source.py @@ -1,6 +1,5 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - from ..._models import BaseModel __all__ = ["Source"] diff --git a/src/writerai/types/tool_parse_pdf_response.py b/src/writerai/types/tool_parse_pdf_response.py index d7ccf851..0d601ec8 100644 --- a/src/writerai/types/tool_parse_pdf_response.py +++ b/src/writerai/types/tool_parse_pdf_response.py @@ -1,6 +1,5 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - from .._models import BaseModel __all__ = ["ToolParsePdfResponse"] diff --git a/src/writerai/types/translation_response.py b/src/writerai/types/translation_response.py index bbd2fee3..9d947d8b 100644 --- a/src/writerai/types/translation_response.py +++ b/src/writerai/types/translation_response.py @@ -1,6 +1,5 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - from .._models import BaseModel __all__ = ["TranslationResponse"] diff --git a/src/writerai/types/vision_response.py b/src/writerai/types/vision_response.py index 0fceb4b0..56b65a3e 100644 --- a/src/writerai/types/vision_response.py +++ b/src/writerai/types/vision_response.py @@ -1,6 +1,5 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - from .._models import BaseModel __all__ = ["VisionResponse"] From 8cf83665f1df3f395471b0fb6cf75b8310cec36d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 23 Apr 2025 17:57:44 +0000 Subject: [PATCH 242/399] chore(ci): run on more branches and use depot runners --- .github/workflows/ci.yml | 16 ++++++++-------- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/release-doctor.yml | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 04b083ca..33820422 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,18 +1,18 @@ name: CI on: push: - branches: - - main - pull_request: - branches: - - main - - next + branches-ignore: + - 'generated' + - 'codegen/**' + - 'integrated/**' + - 'stl-preview-head/**' + - 'stl-preview-base/**' jobs: lint: timeout-minutes: 10 name: lint - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 steps: - uses: actions/checkout@v4 @@ -33,7 +33,7 @@ jobs: test: timeout-minutes: 10 name: test - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 85424099..a7d454f8 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -11,7 +11,7 @@ on: jobs: publish: name: publish - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index 43d314d3..0cbe46b3 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -8,7 +8,7 @@ on: jobs: release_doctor: name: release doctor - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04 if: github.repository == 'writer/writer-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') steps: From 5637fb9651f8e2ad495c80fcdb2543bfa330f399 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 23 Apr 2025 20:00:14 +0000 Subject: [PATCH 243/399] chore(ci): only use depot for staging repos --- .github/workflows/ci.yml | 4 ++-- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/release-doctor.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33820422..75814a6a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: lint: timeout-minutes: 10 name: lint - runs-on: depot-ubuntu-24.04 + runs-on: ${{ github.repository == 'stainless-sdks/writer-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - uses: actions/checkout@v4 @@ -33,7 +33,7 @@ jobs: test: timeout-minutes: 10 name: test - runs-on: depot-ubuntu-24.04 + runs-on: ${{ github.repository == 'stainless-sdks/writer-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index a7d454f8..85424099 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -11,7 +11,7 @@ on: jobs: publish: name: publish - runs-on: depot-ubuntu-24.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index 0cbe46b3..43d314d3 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -8,7 +8,7 @@ on: jobs: release_doctor: name: release doctor - runs-on: depot-ubuntu-24.04 + runs-on: ubuntu-latest if: github.repository == 'writer/writer-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') steps: From d3a8b2895516994c2ed7fc2798edbc057651c517 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 23 Apr 2025 22:09:47 +0000 Subject: [PATCH 244/399] chore: broadly detect json family of content-type headers --- src/writerai/_response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/writerai/_response.py b/src/writerai/_response.py index 82dfb2e3..2183819a 100644 --- a/src/writerai/_response.py +++ b/src/writerai/_response.py @@ -233,7 +233,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: # split is required to handle cases where additional information is included # in the response, e.g. application/json; charset=utf-8 content_type, *_ = response.headers.get("content-type", "*").split(";") - if content_type != "application/json": + if not content_type.endswith("json"): if is_basemodel(cast_to): try: data = response.json() From 206ac5eeeb559e7c6290b487de78272f851335c4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 25 Apr 2025 18:28:19 +0000 Subject: [PATCH 245/399] docs(api): updates to API spec --- .stats.yml | 4 ++-- src/writerai/types/chat_completion_message.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index a6212227..4208f745 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 32 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-a0e83ebfd81856b538d16c7a41ccdc11687ee558e1435a40f3bb7e0d6e1ab7c8.yml -openapi_spec_hash: 8ac62303a9158c13f344975cb0786bc6 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-9fc9c575a83b15ef4abba45e5d4d676814286faa75e595f82c776b3741c8c8af.yml +openapi_spec_hash: d5c4a0a2343890ad1db2e1df15c09c54 config_hash: b1d3b26784527ee46af49c1bb9e93193 diff --git a/src/writerai/types/chat_completion_message.py b/src/writerai/types/chat_completion_message.py index 162063f7..7bcfe611 100644 --- a/src/writerai/types/chat_completion_message.py +++ b/src/writerai/types/chat_completion_message.py @@ -19,7 +19,7 @@ class LlmData(BaseModel): class ChatCompletionMessage(BaseModel): - content: str + content: object """The text content produced by the model. This field contains the actual output generated, reflecting the model's response From fc1a12326455702a1d398f0960a8e59d96817431 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 25 Apr 2025 20:29:51 +0000 Subject: [PATCH 246/399] docs(api): updates to API spec --- .stats.yml | 4 ++-- src/writerai/types/chat_completion_message.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index 4208f745..a6212227 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 32 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-9fc9c575a83b15ef4abba45e5d4d676814286faa75e595f82c776b3741c8c8af.yml -openapi_spec_hash: d5c4a0a2343890ad1db2e1df15c09c54 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-a0e83ebfd81856b538d16c7a41ccdc11687ee558e1435a40f3bb7e0d6e1ab7c8.yml +openapi_spec_hash: 8ac62303a9158c13f344975cb0786bc6 config_hash: b1d3b26784527ee46af49c1bb9e93193 diff --git a/src/writerai/types/chat_completion_message.py b/src/writerai/types/chat_completion_message.py index 7bcfe611..162063f7 100644 --- a/src/writerai/types/chat_completion_message.py +++ b/src/writerai/types/chat_completion_message.py @@ -19,7 +19,7 @@ class LlmData(BaseModel): class ChatCompletionMessage(BaseModel): - content: object + content: str """The text content produced by the model. This field contains the actual output generated, reflecting the model's response From 74e8d3f60c7306233bcb5fcfd6a370c61c1637d0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 25 Apr 2025 22:44:48 +0000 Subject: [PATCH 247/399] chore(internal): codegen related update --- .release-please-manifest.json | 2 +- README.md | 2 +- pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 656a2ef1..e3487d3a 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.1.0" + ".": "2.2.0-rc1" } \ No newline at end of file diff --git a/README.md b/README.md index 2a343f7f..427ff405 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The REST API documentation can be found on [dev.writer.com](https://dev.writer.c ```sh # install from PyPI -pip install writer-sdk +pip install --pre writer-sdk ``` ## Usage diff --git a/pyproject.toml b/pyproject.toml index 36673967..0955d057 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "2.1.0" +version = "2.2.0-rc1" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index 4443c0f6..1a168d7f 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "2.1.0" # x-release-please-version +__version__ = "2.2.0-rc1" # x-release-please-version From 5c5989fe64969ffebae93e59daae69e141007ee4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 20:56:26 +0000 Subject: [PATCH 248/399] docs(api): updates to API spec --- .stats.yml | 4 +- src/writerai/resources/chat.py | 108 +++++++++--------- src/writerai/resources/completions.py | 12 +- src/writerai/types/chat_chat_params.py | 18 +-- src/writerai/types/chat_completion_chunk.py | 15 ++- src/writerai/types/chat_completion_message.py | 15 ++- .../types/completion_create_params.py | 2 +- src/writerai/types/shared/tool_param.py | 64 ++++++++++- .../types/shared_params/tool_param.py | 64 ++++++++++- 9 files changed, 226 insertions(+), 76 deletions(-) diff --git a/.stats.yml b/.stats.yml index a6212227..0eb43af0 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 32 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-a0e83ebfd81856b538d16c7a41ccdc11687ee558e1435a40f3bb7e0d6e1ab7c8.yml -openapi_spec_hash: 8ac62303a9158c13f344975cb0786bc6 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-07dea48ea85e600712dcdfd99a688f6a9cb8dd1f56d0a06e0ab54fc8a98a89b1.yml +openapi_spec_hash: 0d30ab04c227bf53f3109dc4d861e5dc config_hash: b1d3b26784527ee46af49c1bb9e93193 diff --git a/src/writerai/resources/chat.py b/src/writerai/resources/chat.py index 324b1902..f388d1e8 100644 --- a/src/writerai/resources/chat.py +++ b/src/writerai/resources/chat.py @@ -82,8 +82,8 @@ def chat( the model to respond to. The array must contain at least one message. model: The [ID of the model](https://dev.writer.com/home/models) to use for creating - the chat completion. Supports `palmyra-x-004`, `palmyra-fin`, `palmyra-med`, - `palmyra-creative`, and `palmyra-x-003-instruct`. + the chat completion. Supports `palmyra-x5`, `palmyra-x4`, `palmyra-fin`, + `palmyra-med`, `palmyra-creative`, and `palmyra-x-003-instruct`. logprobs: Specifies whether to return log probabilities of the output tokens. @@ -95,8 +95,8 @@ def chat( single request. This parameter allows for generating multiple responses, offering a variety of potential replies from which to choose. - response_format: The response format to use for the chat completion, available with - `palmyra-x-004`. + response_format: The response format to use for the chat completion, available with `palmyra-x4` + and `palmyra-x5`. `text` is the default response format. [JSON Schema](https://json-schema.org/) is supported for structured responses. If you specify `json_schema`, you must @@ -122,11 +122,11 @@ def chat( tools: An array containing tool definitions for tools that the model can use to generate responses. The tool definitions use JSON schema. You can define your - own functions or use one of the built-in `graph`, `llm`, or `vision` tools. Note - that you can only use one built-in tool type in the array (only one of `graph`, - `llm`, or `vision`). You can pass multiple custom - tools](https://dev.writer.com/api-guides/tool-calling) of type `function` in the - same request. + own functions or use one of the built-in `graph`, `llm`, `translation`, or + `vision` tools. Note that you can only use one built-in tool type in the array + (only one of `graph`, `llm`, `translation`, or `vision`). You can pass multiple + [custom tools](https://dev.writer.com/api-guides/tool-calling) of type + `function` in the same request. top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with @@ -178,8 +178,8 @@ def chat( the model to respond to. The array must contain at least one message. model: The [ID of the model](https://dev.writer.com/home/models) to use for creating - the chat completion. Supports `palmyra-x-004`, `palmyra-fin`, `palmyra-med`, - `palmyra-creative`, and `palmyra-x-003-instruct`. + the chat completion. Supports `palmyra-x5`, `palmyra-x4`, `palmyra-fin`, + `palmyra-med`, `palmyra-creative`, and `palmyra-x-003-instruct`. stream: Indicates whether the response should be streamed incrementally as it is generated or only returned once fully complete. Streaming can be useful for @@ -195,8 +195,8 @@ def chat( single request. This parameter allows for generating multiple responses, offering a variety of potential replies from which to choose. - response_format: The response format to use for the chat completion, available with - `palmyra-x-004`. + response_format: The response format to use for the chat completion, available with `palmyra-x4` + and `palmyra-x5`. `text` is the default response format. [JSON Schema](https://json-schema.org/) is supported for structured responses. If you specify `json_schema`, you must @@ -218,11 +218,11 @@ def chat( tools: An array containing tool definitions for tools that the model can use to generate responses. The tool definitions use JSON schema. You can define your - own functions or use one of the built-in `graph`, `llm`, or `vision` tools. Note - that you can only use one built-in tool type in the array (only one of `graph`, - `llm`, or `vision`). You can pass multiple custom - tools](https://dev.writer.com/api-guides/tool-calling) of type `function` in the - same request. + own functions or use one of the built-in `graph`, `llm`, `translation`, or + `vision` tools. Note that you can only use one built-in tool type in the array + (only one of `graph`, `llm`, `translation`, or `vision`). You can pass multiple + [custom tools](https://dev.writer.com/api-guides/tool-calling) of type + `function` in the same request. top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with @@ -274,8 +274,8 @@ def chat( the model to respond to. The array must contain at least one message. model: The [ID of the model](https://dev.writer.com/home/models) to use for creating - the chat completion. Supports `palmyra-x-004`, `palmyra-fin`, `palmyra-med`, - `palmyra-creative`, and `palmyra-x-003-instruct`. + the chat completion. Supports `palmyra-x5`, `palmyra-x4`, `palmyra-fin`, + `palmyra-med`, `palmyra-creative`, and `palmyra-x-003-instruct`. stream: Indicates whether the response should be streamed incrementally as it is generated or only returned once fully complete. Streaming can be useful for @@ -291,8 +291,8 @@ def chat( single request. This parameter allows for generating multiple responses, offering a variety of potential replies from which to choose. - response_format: The response format to use for the chat completion, available with - `palmyra-x-004`. + response_format: The response format to use for the chat completion, available with `palmyra-x4` + and `palmyra-x5`. `text` is the default response format. [JSON Schema](https://json-schema.org/) is supported for structured responses. If you specify `json_schema`, you must @@ -314,11 +314,11 @@ def chat( tools: An array containing tool definitions for tools that the model can use to generate responses. The tool definitions use JSON schema. You can define your - own functions or use one of the built-in `graph`, `llm`, or `vision` tools. Note - that you can only use one built-in tool type in the array (only one of `graph`, - `llm`, or `vision`). You can pass multiple custom - tools](https://dev.writer.com/api-guides/tool-calling) of type `function` in the - same request. + own functions or use one of the built-in `graph`, `llm`, `translation`, or + `vision` tools. Note that you can only use one built-in tool type in the array + (only one of `graph`, `llm`, `translation`, or `vision`). You can pass multiple + [custom tools](https://dev.writer.com/api-guides/tool-calling) of type + `function` in the same request. top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with @@ -443,8 +443,8 @@ async def chat( the model to respond to. The array must contain at least one message. model: The [ID of the model](https://dev.writer.com/home/models) to use for creating - the chat completion. Supports `palmyra-x-004`, `palmyra-fin`, `palmyra-med`, - `palmyra-creative`, and `palmyra-x-003-instruct`. + the chat completion. Supports `palmyra-x5`, `palmyra-x4`, `palmyra-fin`, + `palmyra-med`, `palmyra-creative`, and `palmyra-x-003-instruct`. logprobs: Specifies whether to return log probabilities of the output tokens. @@ -456,8 +456,8 @@ async def chat( single request. This parameter allows for generating multiple responses, offering a variety of potential replies from which to choose. - response_format: The response format to use for the chat completion, available with - `palmyra-x-004`. + response_format: The response format to use for the chat completion, available with `palmyra-x4` + and `palmyra-x5`. `text` is the default response format. [JSON Schema](https://json-schema.org/) is supported for structured responses. If you specify `json_schema`, you must @@ -483,11 +483,11 @@ async def chat( tools: An array containing tool definitions for tools that the model can use to generate responses. The tool definitions use JSON schema. You can define your - own functions or use one of the built-in `graph`, `llm`, or `vision` tools. Note - that you can only use one built-in tool type in the array (only one of `graph`, - `llm`, or `vision`). You can pass multiple custom - tools](https://dev.writer.com/api-guides/tool-calling) of type `function` in the - same request. + own functions or use one of the built-in `graph`, `llm`, `translation`, or + `vision` tools. Note that you can only use one built-in tool type in the array + (only one of `graph`, `llm`, `translation`, or `vision`). You can pass multiple + [custom tools](https://dev.writer.com/api-guides/tool-calling) of type + `function` in the same request. top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with @@ -539,8 +539,8 @@ async def chat( the model to respond to. The array must contain at least one message. model: The [ID of the model](https://dev.writer.com/home/models) to use for creating - the chat completion. Supports `palmyra-x-004`, `palmyra-fin`, `palmyra-med`, - `palmyra-creative`, and `palmyra-x-003-instruct`. + the chat completion. Supports `palmyra-x5`, `palmyra-x4`, `palmyra-fin`, + `palmyra-med`, `palmyra-creative`, and `palmyra-x-003-instruct`. stream: Indicates whether the response should be streamed incrementally as it is generated or only returned once fully complete. Streaming can be useful for @@ -556,8 +556,8 @@ async def chat( single request. This parameter allows for generating multiple responses, offering a variety of potential replies from which to choose. - response_format: The response format to use for the chat completion, available with - `palmyra-x-004`. + response_format: The response format to use for the chat completion, available with `palmyra-x4` + and `palmyra-x5`. `text` is the default response format. [JSON Schema](https://json-schema.org/) is supported for structured responses. If you specify `json_schema`, you must @@ -579,11 +579,11 @@ async def chat( tools: An array containing tool definitions for tools that the model can use to generate responses. The tool definitions use JSON schema. You can define your - own functions or use one of the built-in `graph`, `llm`, or `vision` tools. Note - that you can only use one built-in tool type in the array (only one of `graph`, - `llm`, or `vision`). You can pass multiple custom - tools](https://dev.writer.com/api-guides/tool-calling) of type `function` in the - same request. + own functions or use one of the built-in `graph`, `llm`, `translation`, or + `vision` tools. Note that you can only use one built-in tool type in the array + (only one of `graph`, `llm`, `translation`, or `vision`). You can pass multiple + [custom tools](https://dev.writer.com/api-guides/tool-calling) of type + `function` in the same request. top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with @@ -635,8 +635,8 @@ async def chat( the model to respond to. The array must contain at least one message. model: The [ID of the model](https://dev.writer.com/home/models) to use for creating - the chat completion. Supports `palmyra-x-004`, `palmyra-fin`, `palmyra-med`, - `palmyra-creative`, and `palmyra-x-003-instruct`. + the chat completion. Supports `palmyra-x5`, `palmyra-x4`, `palmyra-fin`, + `palmyra-med`, `palmyra-creative`, and `palmyra-x-003-instruct`. stream: Indicates whether the response should be streamed incrementally as it is generated or only returned once fully complete. Streaming can be useful for @@ -652,8 +652,8 @@ async def chat( single request. This parameter allows for generating multiple responses, offering a variety of potential replies from which to choose. - response_format: The response format to use for the chat completion, available with - `palmyra-x-004`. + response_format: The response format to use for the chat completion, available with `palmyra-x4` + and `palmyra-x5`. `text` is the default response format. [JSON Schema](https://json-schema.org/) is supported for structured responses. If you specify `json_schema`, you must @@ -675,11 +675,11 @@ async def chat( tools: An array containing tool definitions for tools that the model can use to generate responses. The tool definitions use JSON schema. You can define your - own functions or use one of the built-in `graph`, `llm`, or `vision` tools. Note - that you can only use one built-in tool type in the array (only one of `graph`, - `llm`, or `vision`). You can pass multiple custom - tools](https://dev.writer.com/api-guides/tool-calling) of type `function` in the - same request. + own functions or use one of the built-in `graph`, `llm`, `translation`, or + `vision` tools. Note that you can only use one built-in tool type in the array + (only one of `graph`, `llm`, `translation`, or `vision`). You can pass multiple + [custom tools](https://dev.writer.com/api-guides/tool-calling) of type + `function` in the same request. top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with diff --git a/src/writerai/resources/completions.py b/src/writerai/resources/completions.py index 5666d30a..4a1c971e 100644 --- a/src/writerai/resources/completions.py +++ b/src/writerai/resources/completions.py @@ -71,7 +71,7 @@ def create( Args: model: The [ID of the model](https://dev.writer.com/home/models) to use for generating - text. Supports `palmyra-x-004`, `palmyra-fin`, `palmyra-med`, + text. Supports `palmyra-x5`, `palmyra-x4`, `palmyra-fin`, `palmyra-med`, `palmyra-creative`, and `palmyra-x-003-instruct`. prompt: The input text that the model will process to generate a response. @@ -134,7 +134,7 @@ def create( Args: model: The [ID of the model](https://dev.writer.com/home/models) to use for generating - text. Supports `palmyra-x-004`, `palmyra-fin`, `palmyra-med`, + text. Supports `palmyra-x5`, `palmyra-x4`, `palmyra-fin`, `palmyra-med`, `palmyra-creative`, and `palmyra-x-003-instruct`. prompt: The input text that the model will process to generate a response. @@ -197,7 +197,7 @@ def create( Args: model: The [ID of the model](https://dev.writer.com/home/models) to use for generating - text. Supports `palmyra-x-004`, `palmyra-fin`, `palmyra-med`, + text. Supports `palmyra-x5`, `palmyra-x4`, `palmyra-fin`, `palmyra-med`, `palmyra-creative`, and `palmyra-x-003-instruct`. prompt: The input text that the model will process to generate a response. @@ -327,7 +327,7 @@ async def create( Args: model: The [ID of the model](https://dev.writer.com/home/models) to use for generating - text. Supports `palmyra-x-004`, `palmyra-fin`, `palmyra-med`, + text. Supports `palmyra-x5`, `palmyra-x4`, `palmyra-fin`, `palmyra-med`, `palmyra-creative`, and `palmyra-x-003-instruct`. prompt: The input text that the model will process to generate a response. @@ -390,7 +390,7 @@ async def create( Args: model: The [ID of the model](https://dev.writer.com/home/models) to use for generating - text. Supports `palmyra-x-004`, `palmyra-fin`, `palmyra-med`, + text. Supports `palmyra-x5`, `palmyra-x4`, `palmyra-fin`, `palmyra-med`, `palmyra-creative`, and `palmyra-x-003-instruct`. prompt: The input text that the model will process to generate a response. @@ -453,7 +453,7 @@ async def create( Args: model: The [ID of the model](https://dev.writer.com/home/models) to use for generating - text. Supports `palmyra-x-004`, `palmyra-fin`, `palmyra-med`, + text. Supports `palmyra-x5`, `palmyra-x4`, `palmyra-fin`, `palmyra-med`, `palmyra-creative`, and `palmyra-x-003-instruct`. prompt: The input text that the model will process to generate a response. diff --git a/src/writerai/types/chat_chat_params.py b/src/writerai/types/chat_chat_params.py index dfc945e4..66b9a544 100644 --- a/src/writerai/types/chat_chat_params.py +++ b/src/writerai/types/chat_chat_params.py @@ -32,8 +32,8 @@ class ChatChatParamsBase(TypedDict, total=False): model: Required[str] """ The [ID of the model](https://dev.writer.com/home/models) to use for creating - the chat completion. Supports `palmyra-x-004`, `palmyra-fin`, `palmyra-med`, - `palmyra-creative`, and `palmyra-x-003-instruct`. + the chat completion. Supports `palmyra-x5`, `palmyra-x4`, `palmyra-fin`, + `palmyra-med`, `palmyra-creative`, and `palmyra-x-003-instruct`. """ logprobs: bool @@ -55,8 +55,8 @@ class ChatChatParamsBase(TypedDict, total=False): response_format: ResponseFormat """ - The response format to use for the chat completion, available with - `palmyra-x-004`. + The response format to use for the chat completion, available with `palmyra-x4` + and `palmyra-x5`. `text` is the default response format. [JSON Schema](https://json-schema.org/) is supported for structured responses. If you specify `json_schema`, you must @@ -91,11 +91,11 @@ class ChatChatParamsBase(TypedDict, total=False): """ An array containing tool definitions for tools that the model can use to generate responses. The tool definitions use JSON schema. You can define your - own functions or use one of the built-in `graph`, `llm`, or `vision` tools. Note - that you can only use one built-in tool type in the array (only one of `graph`, - `llm`, or `vision`). You can pass multiple custom - tools](https://dev.writer.com/api-guides/tool-calling) of type `function` in the - same request. + own functions or use one of the built-in `graph`, `llm`, `translation`, or + `vision` tools. Note that you can only use one built-in tool type in the array + (only one of `graph`, `llm`, `translation`, or `vision`). You can pass multiple + [custom tools](https://dev.writer.com/api-guides/tool-calling) of type + `function` in the same request. """ top_p: float diff --git a/src/writerai/types/chat_completion_chunk.py b/src/writerai/types/chat_completion_chunk.py index 8d9deb37..6d873355 100644 --- a/src/writerai/types/chat_completion_chunk.py +++ b/src/writerai/types/chat_completion_chunk.py @@ -10,7 +10,7 @@ from .chat_completion_message import ChatCompletionMessage from .shared.tool_call_streaming import ToolCallStreaming -__all__ = ["ChatCompletionChunk", "Choice", "ChoiceDelta", "ChoiceDeltaLlmData"] +__all__ = ["ChatCompletionChunk", "Choice", "ChoiceDelta", "ChoiceDeltaLlmData", "ChoiceDeltaTranslationData"] class ChoiceDeltaLlmData(BaseModel): @@ -21,6 +21,17 @@ class ChoiceDeltaLlmData(BaseModel): """The prompt processed by the model.""" +class ChoiceDeltaTranslationData(BaseModel): + source_language_code: str + """The language code of the source text.""" + + source_text: str + """The text the tool translated.""" + + target_language_code: str + """The language code of the target text.""" + + class ChoiceDelta(BaseModel): content: Optional[str] = None """The text content produced by the model. @@ -44,6 +55,8 @@ class ChoiceDelta(BaseModel): tool_calls: Optional[List[ToolCallStreaming]] = None + translation_data: Optional[ChoiceDeltaTranslationData] = None + class Choice(BaseModel): delta: ChoiceDelta diff --git a/src/writerai/types/chat_completion_message.py b/src/writerai/types/chat_completion_message.py index 162063f7..acf5649d 100644 --- a/src/writerai/types/chat_completion_message.py +++ b/src/writerai/types/chat_completion_message.py @@ -7,7 +7,7 @@ from .shared.tool_call import ToolCall from .shared.graph_data import GraphData -__all__ = ["ChatCompletionMessage", "LlmData"] +__all__ = ["ChatCompletionMessage", "LlmData", "TranslationData"] class LlmData(BaseModel): @@ -18,6 +18,17 @@ class LlmData(BaseModel): """The prompt processed by the model.""" +class TranslationData(BaseModel): + source_language_code: str + """The language code of the source text.""" + + source_text: str + """The text the tool translated.""" + + target_language_code: str + """The language code of the target text.""" + + class ChatCompletionMessage(BaseModel): content: str """The text content produced by the model. @@ -36,3 +47,5 @@ class ChatCompletionMessage(BaseModel): llm_data: Optional[LlmData] = None tool_calls: Optional[List[ToolCall]] = None + + translation_data: Optional[TranslationData] = None diff --git a/src/writerai/types/completion_create_params.py b/src/writerai/types/completion_create_params.py index e55ffba5..b327c132 100644 --- a/src/writerai/types/completion_create_params.py +++ b/src/writerai/types/completion_create_params.py @@ -12,7 +12,7 @@ class CompletionCreateParamsBase(TypedDict, total=False): model: Required[str] """ The [ID of the model](https://dev.writer.com/home/models) to use for generating - text. Supports `palmyra-x-004`, `palmyra-fin`, `palmyra-med`, + text. Supports `palmyra-x5`, `palmyra-x4`, `palmyra-fin`, `palmyra-med`, `palmyra-creative`, and `palmyra-x-003-instruct`. """ diff --git a/src/writerai/types/shared/tool_param.py b/src/writerai/types/shared/tool_param.py index 3d03f382..aa76ed45 100644 --- a/src/writerai/types/shared/tool_param.py +++ b/src/writerai/types/shared/tool_param.py @@ -14,6 +14,8 @@ "GraphToolFunction", "LlmTool", "LlmToolFunction", + "TranslationTool", + "TranslationToolFunction", "VisionTool", "VisionToolFunction", "VisionToolFunctionVariable", @@ -63,6 +65,66 @@ class LlmTool(BaseModel): """The type of tool.""" +class TranslationToolFunction(BaseModel): + formality: bool + """Whether to use formal or informal language in the translation. + + See the + [list of languages that support formality](https://dev.writer.com/api-guides/api-reference/translation-api/language-support#formality). + If the language does not support formality, this parameter is ignored. + """ + + length_control: bool + """Whether to control the length of the translated text. + + See the + [list of languages that support length control](https://dev.writer.com/api-guides/api-reference/translation-api/language-support#length-control). + If the language does not support length control, this parameter is ignored. + """ + + mask_profanity: bool + """Whether to mask profane words in the translated text. + + See the + [list of languages that do not support profanity masking](https://dev.writer.com/api-guides/api-reference/translation-api/language-support#profanity-masking). + If the language does not support profanity masking, this parameter is ignored. + """ + + model: Literal["palmyra-translate"] + """The model to use for translation.""" + + source_language_code: Optional[str] = None + """Optional. + + The [ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes) + language code of the original text to translate. For example, `en` for English, + `zh` for Chinese, `fr` for French, `es` for Spanish. If the language has a + variant, the code appends the two-digit + [ISO-3166 country code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes). + If you do not provide a language code, the LLM detects the language of the text. + """ + + target_language_code: Optional[str] = None + """Optional. + + The [ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes) + language code of the target language for the translation. For example, `en` for + English, `zh` for Chinese, `fr` for French, `es` for Spanish. If the language + has a variant, the code appends the two-digit + [ISO-3166 country code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes). + If you do not provide a language code, the LLM uses the content of the chat + message to determine the target language. + """ + + +class TranslationTool(BaseModel): + function: TranslationToolFunction + """A tool that uses Palmyra Translate to translate text.""" + + type: Literal["translation"] + """The type of tool.""" + + class VisionToolFunctionVariable(BaseModel): file_id: str """The File ID of the image to analyze. @@ -97,5 +159,5 @@ class VisionTool(BaseModel): ToolParam: TypeAlias = Annotated[ - Union[FunctionTool, GraphTool, LlmTool, VisionTool], PropertyInfo(discriminator="type") + Union[FunctionTool, GraphTool, LlmTool, TranslationTool, VisionTool], PropertyInfo(discriminator="type") ] diff --git a/src/writerai/types/shared_params/tool_param.py b/src/writerai/types/shared_params/tool_param.py index 902b74f7..c50863e4 100644 --- a/src/writerai/types/shared_params/tool_param.py +++ b/src/writerai/types/shared_params/tool_param.py @@ -14,6 +14,8 @@ "GraphToolFunction", "LlmTool", "LlmToolFunction", + "TranslationTool", + "TranslationToolFunction", "VisionTool", "VisionToolFunction", "VisionToolFunctionVariable", @@ -63,6 +65,66 @@ class LlmTool(TypedDict, total=False): """The type of tool.""" +class TranslationToolFunction(TypedDict, total=False): + formality: Required[bool] + """Whether to use formal or informal language in the translation. + + See the + [list of languages that support formality](https://dev.writer.com/api-guides/api-reference/translation-api/language-support#formality). + If the language does not support formality, this parameter is ignored. + """ + + length_control: Required[bool] + """Whether to control the length of the translated text. + + See the + [list of languages that support length control](https://dev.writer.com/api-guides/api-reference/translation-api/language-support#length-control). + If the language does not support length control, this parameter is ignored. + """ + + mask_profanity: Required[bool] + """Whether to mask profane words in the translated text. + + See the + [list of languages that do not support profanity masking](https://dev.writer.com/api-guides/api-reference/translation-api/language-support#profanity-masking). + If the language does not support profanity masking, this parameter is ignored. + """ + + model: Required[Literal["palmyra-translate"]] + """The model to use for translation.""" + + source_language_code: str + """Optional. + + The [ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes) + language code of the original text to translate. For example, `en` for English, + `zh` for Chinese, `fr` for French, `es` for Spanish. If the language has a + variant, the code appends the two-digit + [ISO-3166 country code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes). + If you do not provide a language code, the LLM detects the language of the text. + """ + + target_language_code: str + """Optional. + + The [ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes) + language code of the target language for the translation. For example, `en` for + English, `zh` for Chinese, `fr` for French, `es` for Spanish. If the language + has a variant, the code appends the two-digit + [ISO-3166 country code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes). + If you do not provide a language code, the LLM uses the content of the chat + message to determine the target language. + """ + + +class TranslationTool(TypedDict, total=False): + function: Required[TranslationToolFunction] + """A tool that uses Palmyra Translate to translate text.""" + + type: Required[Literal["translation"]] + """The type of tool.""" + + class VisionToolFunctionVariable(TypedDict, total=False): file_id: Required[str] """The File ID of the image to analyze. @@ -96,4 +158,4 @@ class VisionTool(TypedDict, total=False): """The type of tool.""" -ToolParam: TypeAlias = Union[FunctionTool, GraphTool, LlmTool, VisionTool] +ToolParam: TypeAlias = Union[FunctionTool, GraphTool, LlmTool, TranslationTool, VisionTool] From 1b0ea1563bf39fd1c2869a94a70b14af36da7f2c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 17:12:40 +0000 Subject: [PATCH 249/399] docs: README updates --- .stats.yml | 2 +- README.md | 18 +++++++++--------- tests/test_client.py | 8 ++++---- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.stats.yml b/.stats.yml index 0eb43af0..6d11c6b0 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 32 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-07dea48ea85e600712dcdfd99a688f6a9cb8dd1f56d0a06e0ab54fc8a98a89b1.yml openapi_spec_hash: 0d30ab04c227bf53f3109dc4d861e5dc -config_hash: b1d3b26784527ee46af49c1bb9e93193 +config_hash: c0c9f57ab19252f82cf765939edc61de diff --git a/README.md b/README.md index 427ff405..3e64a437 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ chat_completion = client.chat.chat( "role": "user", } ], - model="palmyra-x-004", + model="palmyra-x5", ) print(chat_completion.id) ``` @@ -70,7 +70,7 @@ async def main() -> None: "role": "user", } ], - model="palmyra-x-004", + model="palmyra-x5", ) print(chat_completion.id) @@ -96,7 +96,7 @@ stream = client.chat.chat( "role": "user", } ], - model="palmyra-x-004", + model="palmyra-x5", stream=True, ) for chat_completion in stream: @@ -117,7 +117,7 @@ stream = await client.chat.chat( "role": "user", } ], - model="palmyra-x-004", + model="palmyra-x5", stream=True, ) async for chat_completion in stream: @@ -239,7 +239,7 @@ try: "role": "user", } ], - model="palmyra-x-004", + model="palmyra-x5", ) except writerai.APIConnectionError as e: print("The server could not be reached") @@ -290,7 +290,7 @@ client.with_options(max_retries=5).chat.chat( "role": "user", } ], - model="palmyra-x-004", + model="palmyra-x5", ) ``` @@ -321,7 +321,7 @@ client.with_options(timeout=5.0).chat.chat( "role": "user", } ], - model="palmyra-x-004", + model="palmyra-x5", ) ``` @@ -368,7 +368,7 @@ response = client.chat.with_raw_response.chat( "content": "Write a haiku about programming", "role": "user", }], - model="palmyra-x-004", + model="palmyra-x5", ) print(response.headers.get('X-My-Header')) @@ -394,7 +394,7 @@ with client.chat.with_streaming_response.chat( "role": "user", } ], - model="palmyra-x-004", + model="palmyra-x5", ) as response: print(response.headers.get("X-My-Header")) diff --git a/tests/test_client.py b/tests/test_client.py index c43a200d..47014385 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -739,7 +739,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> No "role": "user", } ], - model="palmyra-x-004", + model="palmyra-x5", ), ChatChatParamsNonStreaming, ), @@ -768,7 +768,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> Non "role": "user", } ], - model="palmyra-x-004", + model="palmyra-x5", ), ChatChatParamsNonStreaming, ), @@ -1559,7 +1559,7 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) "role": "user", } ], - model="palmyra-x-004", + model="palmyra-x5", ), ChatChatParamsNonStreaming, ), @@ -1588,7 +1588,7 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) "role": "user", } ], - model="palmyra-x-004", + model="palmyra-x5", ), ChatChatParamsNonStreaming, ), From 24b9fdea5323e54efa77dfa621efe47ef29fd536 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 19:08:31 +0000 Subject: [PATCH 250/399] chore(internal): codegen related update --- .release-please-manifest.json | 2 +- README.md | 2 +- pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index e3487d3a..bfc26f9c 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.2.0-rc1" + ".": "2.2.0" } \ No newline at end of file diff --git a/README.md b/README.md index 3e64a437..cea8efa9 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The REST API documentation can be found on [dev.writer.com](https://dev.writer.c ```sh # install from PyPI -pip install --pre writer-sdk +pip install writer-sdk ``` ## Usage diff --git a/pyproject.toml b/pyproject.toml index 0955d057..c3ee239f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "2.2.0-rc1" +version = "2.2.0" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index 1a168d7f..3033a06d 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "2.2.0-rc1" # x-release-please-version +__version__ = "2.2.0" # x-release-please-version From 011b1715325a52531c9e80096739a091b8863a72 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 8 May 2025 12:42:34 +0000 Subject: [PATCH 251/399] chore(internal): avoid errors for isinstance checks on proxies --- src/writerai/_utils/_proxy.py | 5 ++++- tests/test_utils/test_proxy.py | 11 +++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/writerai/_utils/_proxy.py b/src/writerai/_utils/_proxy.py index ffd883e9..0f239a33 100644 --- a/src/writerai/_utils/_proxy.py +++ b/src/writerai/_utils/_proxy.py @@ -46,7 +46,10 @@ def __dir__(self) -> Iterable[str]: @property # type: ignore @override def __class__(self) -> type: # pyright: ignore - proxied = self.__get_proxied__() + try: + proxied = self.__get_proxied__() + except Exception: + return type(self) if issubclass(type(proxied), LazyProxy): return type(proxied) return proxied.__class__ diff --git a/tests/test_utils/test_proxy.py b/tests/test_utils/test_proxy.py index 449f727d..8d75d83d 100644 --- a/tests/test_utils/test_proxy.py +++ b/tests/test_utils/test_proxy.py @@ -21,3 +21,14 @@ def test_recursive_proxy() -> None: assert dir(proxy) == [] assert type(proxy).__name__ == "RecursiveLazyProxy" assert type(operator.attrgetter("name.foo.bar.baz")(proxy)).__name__ == "RecursiveLazyProxy" + + +def test_isinstance_does_not_error() -> None: + class AlwaysErrorProxy(LazyProxy[Any]): + @override + def __load__(self) -> Any: + raise RuntimeError("Mocking missing dependency") + + proxy = AlwaysErrorProxy() + assert not isinstance(proxy, dict) + assert isinstance(proxy, LazyProxy) From a4f43444baabccdeb3f47d8486208ad9b8cdf8f6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 9 May 2025 10:57:04 +0000 Subject: [PATCH 252/399] chore(internal): avoid lint errors in pagination expressions --- src/writerai/pagination.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/writerai/pagination.py b/src/writerai/pagination.py index a68ce8b3..d8534f51 100644 --- a/src/writerai/pagination.py +++ b/src/writerai/pagination.py @@ -129,7 +129,7 @@ def next_page_info(self) -> Optional[PageInfo]: if self.pagination.offset is not None: offset = self.pagination.offset if offset is None: - return None + return None # type: ignore[unreachable] length = len(self._get_page_items()) current_count = offset + length @@ -163,7 +163,7 @@ def next_page_info(self) -> Optional[PageInfo]: if self.pagination.offset is not None: offset = self.pagination.offset if offset is None: - return None + return None # type: ignore[unreachable] length = len(self._get_page_items()) current_count = offset + length From 1b99c145a204119c51e27d70b6955ba04b8033dc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 9 May 2025 14:19:13 +0000 Subject: [PATCH 253/399] fix(package): support direct resource imports --- src/writerai/__init__.py | 5 +++++ src/writerai/_utils/_resources_proxy.py | 24 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 src/writerai/_utils/_resources_proxy.py diff --git a/src/writerai/__init__.py b/src/writerai/__init__.py index 77e4947c..289aabb4 100644 --- a/src/writerai/__init__.py +++ b/src/writerai/__init__.py @@ -1,5 +1,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +import typing as _t + from . import types from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes from ._utils import file_from_path @@ -68,6 +70,9 @@ "DefaultAsyncHttpxClient", ] +if not _t.TYPE_CHECKING: + from ._utils._resources_proxy import resources as resources + _setup_logging() # Update the __module__ attribute for exported symbols so that diff --git a/src/writerai/_utils/_resources_proxy.py b/src/writerai/_utils/_resources_proxy.py new file mode 100644 index 00000000..200626ef --- /dev/null +++ b/src/writerai/_utils/_resources_proxy.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from typing import Any +from typing_extensions import override + +from ._proxy import LazyProxy + + +class ResourcesProxy(LazyProxy[Any]): + """A proxy for the `writerai.resources` module. + + This is used so that we can lazily import `writerai.resources` only when + needed *and* so that users can just import `writerai` and reference `writerai.resources` + """ + + @override + def __load__(self) -> Any: + import importlib + + mod = importlib.import_module("writerai.resources") + return mod + + +resources = ResourcesProxy().__as_proxied__() From 463e7d3cf7a42066612d2e614497429e6ceac45b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 9 May 2025 17:50:40 +0000 Subject: [PATCH 254/399] docs(api): updates to API spec --- .stats.yml | 4 ++-- src/writerai/resources/files.py | 10 ++++++++++ src/writerai/types/file_list_params.py | 6 ++++++ tests/api_resources/test_files.py | 2 ++ 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 6d11c6b0..cb4f89b7 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 32 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-07dea48ea85e600712dcdfd99a688f6a9cb8dd1f56d0a06e0ab54fc8a98a89b1.yml -openapi_spec_hash: 0d30ab04c227bf53f3109dc4d861e5dc +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-86cece3af9ac57e8d575e7e1a9fbb834f7ea6a635267ba6e1a87f277293d8e85.yml +openapi_spec_hash: 3e7e420d10e8ad4a68db3c0877e7c949 config_hash: c0c9f57ab19252f82cf765939edc61de diff --git a/src/writerai/resources/files.py b/src/writerai/resources/files.py index 3cc4a746..774957a1 100644 --- a/src/writerai/resources/files.py +++ b/src/writerai/resources/files.py @@ -93,6 +93,7 @@ def list( *, after: str | NotGiven = NOT_GIVEN, before: str | NotGiven = NOT_GIVEN, + file_type: str | NotGiven = NOT_GIVEN, graph_id: str | NotGiven = NOT_GIVEN, limit: int | NotGiven = NOT_GIVEN, order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, @@ -115,6 +116,9 @@ def list( before: The ID of the first object in the previous page. This parameter instructs the API to return the previous page of results. + file_type: The extensions of the files to retrieve. Separate multiple extensions with a + comma. For example: `pdf,jpg,docx`. + graph_id: The unique identifier of the graph to which the files belong. limit: Specifies the maximum number of objects returned in a page. The default value @@ -146,6 +150,7 @@ def list( { "after": after, "before": before, + "file_type": file_type, "graph_id": graph_id, "limit": limit, "order": order, @@ -351,6 +356,7 @@ def list( *, after: str | NotGiven = NOT_GIVEN, before: str | NotGiven = NOT_GIVEN, + file_type: str | NotGiven = NOT_GIVEN, graph_id: str | NotGiven = NOT_GIVEN, limit: int | NotGiven = NOT_GIVEN, order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, @@ -373,6 +379,9 @@ def list( before: The ID of the first object in the previous page. This parameter instructs the API to return the previous page of results. + file_type: The extensions of the files to retrieve. Separate multiple extensions with a + comma. For example: `pdf,jpg,docx`. + graph_id: The unique identifier of the graph to which the files belong. limit: Specifies the maximum number of objects returned in a page. The default value @@ -404,6 +413,7 @@ def list( { "after": after, "before": before, + "file_type": file_type, "graph_id": graph_id, "limit": limit, "order": order, diff --git a/src/writerai/types/file_list_params.py b/src/writerai/types/file_list_params.py index 00ad5b97..2af7bfa5 100644 --- a/src/writerai/types/file_list_params.py +++ b/src/writerai/types/file_list_params.py @@ -20,6 +20,12 @@ class FileListParams(TypedDict, total=False): This parameter instructs the API to return the previous page of results. """ + file_type: str + """The extensions of the files to retrieve. + + Separate multiple extensions with a comma. For example: `pdf,jpg,docx`. + """ + graph_id: str """The unique identifier of the graph to which the files belong.""" diff --git a/tests/api_resources/test_files.py b/tests/api_resources/test_files.py index e0bf2ce8..2eb84924 100644 --- a/tests/api_resources/test_files.py +++ b/tests/api_resources/test_files.py @@ -78,6 +78,7 @@ def test_method_list_with_all_params(self, client: Writer) -> None: file = client.files.list( after="after", before="before", + file_type="file_type", graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", limit=0, order="asc", @@ -317,6 +318,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncWriter) -> N file = await async_client.files.list( after="after", before="before", + file_type="file_type", graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", limit=0, order="asc", From cd9bf4f9241dd8ef987a81f58f15127090977d68 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 9 May 2025 18:13:58 +0000 Subject: [PATCH 255/399] docs(api): updates to API spec --- .stats.yml | 4 ++-- src/writerai/resources/files.py | 12 ++++++------ src/writerai/types/file_list_params.py | 2 +- tests/api_resources/test_files.py | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.stats.yml b/.stats.yml index cb4f89b7..38d9f82d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 32 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-86cece3af9ac57e8d575e7e1a9fbb834f7ea6a635267ba6e1a87f277293d8e85.yml -openapi_spec_hash: 3e7e420d10e8ad4a68db3c0877e7c949 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-fab7a71148b6f413a4425ca9f2ce3d42557b65d35ab28c6f64daa7fce6d0ffe2.yml +openapi_spec_hash: 0ead6944545bc40172176e15cc704633 config_hash: c0c9f57ab19252f82cf765939edc61de diff --git a/src/writerai/resources/files.py b/src/writerai/resources/files.py index 774957a1..cd93acc8 100644 --- a/src/writerai/resources/files.py +++ b/src/writerai/resources/files.py @@ -93,7 +93,7 @@ def list( *, after: str | NotGiven = NOT_GIVEN, before: str | NotGiven = NOT_GIVEN, - file_type: str | NotGiven = NOT_GIVEN, + file_types: str | NotGiven = NOT_GIVEN, graph_id: str | NotGiven = NOT_GIVEN, limit: int | NotGiven = NOT_GIVEN, order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, @@ -116,7 +116,7 @@ def list( before: The ID of the first object in the previous page. This parameter instructs the API to return the previous page of results. - file_type: The extensions of the files to retrieve. Separate multiple extensions with a + file_types: The extensions of the files to retrieve. Separate multiple extensions with a comma. For example: `pdf,jpg,docx`. graph_id: The unique identifier of the graph to which the files belong. @@ -150,7 +150,7 @@ def list( { "after": after, "before": before, - "file_type": file_type, + "file_types": file_types, "graph_id": graph_id, "limit": limit, "order": order, @@ -356,7 +356,7 @@ def list( *, after: str | NotGiven = NOT_GIVEN, before: str | NotGiven = NOT_GIVEN, - file_type: str | NotGiven = NOT_GIVEN, + file_types: str | NotGiven = NOT_GIVEN, graph_id: str | NotGiven = NOT_GIVEN, limit: int | NotGiven = NOT_GIVEN, order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, @@ -379,7 +379,7 @@ def list( before: The ID of the first object in the previous page. This parameter instructs the API to return the previous page of results. - file_type: The extensions of the files to retrieve. Separate multiple extensions with a + file_types: The extensions of the files to retrieve. Separate multiple extensions with a comma. For example: `pdf,jpg,docx`. graph_id: The unique identifier of the graph to which the files belong. @@ -413,7 +413,7 @@ def list( { "after": after, "before": before, - "file_type": file_type, + "file_types": file_types, "graph_id": graph_id, "limit": limit, "order": order, diff --git a/src/writerai/types/file_list_params.py b/src/writerai/types/file_list_params.py index 2af7bfa5..8bc101e1 100644 --- a/src/writerai/types/file_list_params.py +++ b/src/writerai/types/file_list_params.py @@ -20,7 +20,7 @@ class FileListParams(TypedDict, total=False): This parameter instructs the API to return the previous page of results. """ - file_type: str + file_types: str """The extensions of the files to retrieve. Separate multiple extensions with a comma. For example: `pdf,jpg,docx`. diff --git a/tests/api_resources/test_files.py b/tests/api_resources/test_files.py index 2eb84924..27622c6b 100644 --- a/tests/api_resources/test_files.py +++ b/tests/api_resources/test_files.py @@ -78,7 +78,7 @@ def test_method_list_with_all_params(self, client: Writer) -> None: file = client.files.list( after="after", before="before", - file_type="file_type", + file_types="file_types", graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", limit=0, order="asc", @@ -318,7 +318,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncWriter) -> N file = await async_client.files.list( after="after", before="before", - file_type="file_type", + file_types="file_types", graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", limit=0, order="asc", From 2dbfb76cd4d5abefa028810a438fc8c977d3b3f5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 23:08:31 +0000 Subject: [PATCH 256/399] chore(ci): upload sdks to package manager --- .github/workflows/ci.yml | 24 ++++++++++++++++++++++++ scripts/utils/upload-artifact.sh | 25 +++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100755 scripts/utils/upload-artifact.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75814a6a..f7d80a9a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,30 @@ jobs: - name: Run lints run: ./scripts/lint + upload: + if: github.repository == 'stainless-sdks/writer-python' + timeout-minutes: 10 + name: upload + permissions: + contents: read + id-token: write + runs-on: depot-ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + - name: Get GitHub OIDC Token + id: github-oidc + uses: actions/github-script@v6 + with: + script: core.setOutput('github_token', await core.getIDToken()); + + - name: Upload tarball + env: + URL: https://pkg.stainless.com/s + AUTH: ${{ steps.github-oidc.outputs.github_token }} + SHA: ${{ github.sha }} + run: ./scripts/utils/upload-artifact.sh + test: timeout-minutes: 10 name: test diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh new file mode 100755 index 00000000..865cd3a0 --- /dev/null +++ b/scripts/utils/upload-artifact.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -exuo pipefail + +RESPONSE=$(curl -X POST "$URL" \ + -H "Authorization: Bearer $AUTH" \ + -H "Content-Type: application/json") + +SIGNED_URL=$(echo "$RESPONSE" | jq -r '.url') + +if [[ "$SIGNED_URL" == "null" ]]; then + echo -e "\033[31mFailed to get signed URL.\033[0m" + exit 1 +fi + +UPLOAD_RESPONSE=$(tar -cz . | curl -v -X PUT \ + -H "Content-Type: application/gzip" \ + --data-binary @- "$SIGNED_URL" 2>&1) + +if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then + echo -e "\033[32mUploaded build to Stainless storage.\033[0m" + echo -e "\033[32mInstallation: npm install 'https://pkg.stainless.com/s/writer-python/$SHA'\033[0m" +else + echo -e "\033[31mFailed to upload artifact.\033[0m" + exit 1 +fi From 438a60d9daebfea3ffd3f5028fb94270c30da3f4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 15 May 2025 12:26:32 +0000 Subject: [PATCH 257/399] chore(ci): fix installation instructions --- scripts/utils/upload-artifact.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh index 865cd3a0..4e270259 100755 --- a/scripts/utils/upload-artifact.sh +++ b/scripts/utils/upload-artifact.sh @@ -18,7 +18,7 @@ UPLOAD_RESPONSE=$(tar -cz . | curl -v -X PUT \ if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then echo -e "\033[32mUploaded build to Stainless storage.\033[0m" - echo -e "\033[32mInstallation: npm install 'https://pkg.stainless.com/s/writer-python/$SHA'\033[0m" + echo -e "\033[32mInstallation: pip install 'https://pkg.stainless.com/s/writer-python/$SHA'\033[0m" else echo -e "\033[31mFailed to upload artifact.\033[0m" exit 1 From 71f3eb10914c2cb35c6e19945bb02cfbfb28add9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 21 May 2025 15:13:00 +0000 Subject: [PATCH 258/399] chore(docs): grammar improvements --- SECURITY.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index b7b1acaa..edf66351 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -16,11 +16,11 @@ before making any information public. ## Reporting Non-SDK Related Security Issues If you encounter security issues that are not directly related to SDKs but pertain to the services -or products provided by Writer please follow the respective company's security reporting guidelines. +or products provided by Writer, please follow the respective company's security reporting guidelines. ### Writer Terms and Policies -Please contact dev-feedback@writer.com for any questions or concerns regarding security of our services. +Please contact dev-feedback@writer.com for any questions or concerns regarding the security of our services. --- From 59bc26b3225aeb6a073c1f7b6c2e4e6cc378c582 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 10:12:43 +0000 Subject: [PATCH 259/399] chore(docs): remove reference to rye shell --- CONTRIBUTING.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 920e3390..47a8ec3e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,8 +17,7 @@ $ rye sync --all-features You can then run scripts using `rye run python script.py` or by activating the virtual environment: ```sh -$ rye shell -# or manually activate - https://docs.python.org/3/library/venv.html#how-venvs-work +# Activate the virtual environment - https://docs.python.org/3/library/venv.html#how-venvs-work $ source .venv/bin/activate # now you can omit the `rye run` prefix From aa54c130a77c1632cc8530f53ab6f3e6d62a4d48 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 10:46:35 +0000 Subject: [PATCH 260/399] chore(docs): remove unnecessary param examples --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index cea8efa9..d6cb821f 100644 --- a/README.md +++ b/README.md @@ -208,10 +208,7 @@ client = Writer() chat_completion = client.chat.chat( messages=[{"role": "user"}], model="model", - response_format={ - "type": "text", - "json_schema": {}, - }, + response_format={"type": "text"}, ) print(chat_completion.response_format) ``` From 5574423ff7a8376e603aaccd54350d38739417e0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 17:34:03 +0000 Subject: [PATCH 261/399] feat(client): add follow_redirects request option --- src/writerai/_base_client.py | 6 ++++ src/writerai/_models.py | 2 ++ src/writerai/_types.py | 2 ++ tests/test_client.py | 54 ++++++++++++++++++++++++++++++++++++ 4 files changed, 64 insertions(+) diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index 078b2770..238e0292 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -960,6 +960,9 @@ def request( if self.custom_auth is not None: kwargs["auth"] = self.custom_auth + if options.follow_redirects is not None: + kwargs["follow_redirects"] = options.follow_redirects + log.debug("Sending HTTP Request: %s %s", request.method, request.url) response = None @@ -1460,6 +1463,9 @@ async def request( if self.custom_auth is not None: kwargs["auth"] = self.custom_auth + if options.follow_redirects is not None: + kwargs["follow_redirects"] = options.follow_redirects + log.debug("Sending HTTP Request: %s %s", request.method, request.url) response = None diff --git a/src/writerai/_models.py b/src/writerai/_models.py index 798956f1..4f214980 100644 --- a/src/writerai/_models.py +++ b/src/writerai/_models.py @@ -737,6 +737,7 @@ class FinalRequestOptionsInput(TypedDict, total=False): idempotency_key: str json_data: Body extra_json: AnyMapping + follow_redirects: bool @final @@ -750,6 +751,7 @@ class FinalRequestOptions(pydantic.BaseModel): files: Union[HttpxRequestFiles, None] = None idempotency_key: Union[str, None] = None post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() + follow_redirects: Union[bool, None] = None # It should be noted that we cannot use `json` here as that would override # a BaseModel method in an incompatible fashion. diff --git a/src/writerai/_types.py b/src/writerai/_types.py index 1b0929e6..cb87a472 100644 --- a/src/writerai/_types.py +++ b/src/writerai/_types.py @@ -100,6 +100,7 @@ class RequestOptions(TypedDict, total=False): params: Query extra_json: AnyMapping idempotency_key: str + follow_redirects: bool # Sentinel class used until PEP 0661 is accepted @@ -215,3 +216,4 @@ class _GenericAlias(Protocol): class HttpxSendArgs(TypedDict, total=False): auth: httpx.Auth + follow_redirects: bool diff --git a/tests/test_client.py b/tests/test_client.py index 47014385..d8a62f66 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -860,6 +860,33 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: assert response.http_request.headers.get("x-stainless-retry-count") == "42" + @pytest.mark.respx(base_url=base_url) + def test_follow_redirects(self, respx_mock: MockRouter) -> None: + # Test that the default follow_redirects=True allows following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"})) + + response = self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) + assert response.status_code == 200 + assert response.json() == {"status": "ok"} + + @pytest.mark.respx(base_url=base_url) + def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None: + # Test that follow_redirects=False prevents following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + + with pytest.raises(APIStatusError) as exc_info: + self.client.post( + "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response + ) + + assert exc_info.value.response.status_code == 302 + assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected" + class TestAsyncWriter: client = AsyncWriter(base_url=base_url, api_key=api_key, _strict_response_validation=True) @@ -1727,3 +1754,30 @@ async def test_main() -> None: raise AssertionError("calling get_platform using asyncify resulted in a hung process") time.sleep(0.1) + + @pytest.mark.respx(base_url=base_url) + async def test_follow_redirects(self, respx_mock: MockRouter) -> None: + # Test that the default follow_redirects=True allows following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"})) + + response = await self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) + assert response.status_code == 200 + assert response.json() == {"status": "ok"} + + @pytest.mark.respx(base_url=base_url) + async def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None: + # Test that follow_redirects=False prevents following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + + with pytest.raises(APIStatusError) as exc_info: + await self.client.post( + "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response + ) + + assert exc_info.value.response.status_code == 302 + assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected" From 7bffb3d68de514a24445e776eb9f9502e987b054 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 12 Jun 2025 10:28:54 +0000 Subject: [PATCH 262/399] chore(tests): run tests in parallel --- pyproject.toml | 3 ++- requirements-dev.lock | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c3ee239f..2016bbb6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ dev-dependencies = [ "importlib-metadata>=6.7.0", "rich>=13.7.1", "nest_asyncio==1.6.0", + "pytest-xdist>=3.6.1", ] [tool.rye.scripts] @@ -125,7 +126,7 @@ replacement = '[\1](https://github.com/writer/writer-python/tree/main/\g<2>)' [tool.pytest.ini_options] testpaths = ["tests"] -addopts = "--tb=short" +addopts = "--tb=short -n auto" xfail_strict = true asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "session" diff --git a/requirements-dev.lock b/requirements-dev.lock index 56d7ab37..3601bc33 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -30,6 +30,8 @@ distro==1.8.0 exceptiongroup==1.2.2 # via anyio # via pytest +execnet==2.1.1 + # via pytest-xdist filelock==3.12.4 # via virtualenv h11==0.14.0 @@ -72,7 +74,9 @@ pygments==2.18.0 pyright==1.1.399 pytest==8.3.3 # via pytest-asyncio + # via pytest-xdist pytest-asyncio==0.24.0 +pytest-xdist==3.7.0 python-dateutil==2.8.2 # via time-machine pytz==2023.3.post1 From 07eea3ba6dc9e58254e4a5c762040d71fe2d7a38 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 12 Jun 2025 16:43:52 +0000 Subject: [PATCH 263/399] fix(client): correctly parse binary response | stream --- src/writerai/_base_client.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index 238e0292..863e7144 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -1071,7 +1071,14 @@ def _process_response( ) -> ResponseT: origin = get_origin(cast_to) or cast_to - if inspect.isclass(origin) and issubclass(origin, BaseAPIResponse): + if ( + inspect.isclass(origin) + and issubclass(origin, BaseAPIResponse) + # we only want to actually return the custom BaseAPIResponse class if we're + # returning the raw response, or if we're not streaming SSE, as if we're streaming + # SSE then `cast_to` doesn't actively reflect the type we need to parse into + and (not stream or bool(response.request.headers.get(RAW_RESPONSE_HEADER))) + ): if not issubclass(origin, APIResponse): raise TypeError(f"API Response types must subclass {APIResponse}; Received {origin}") @@ -1574,7 +1581,14 @@ async def _process_response( ) -> ResponseT: origin = get_origin(cast_to) or cast_to - if inspect.isclass(origin) and issubclass(origin, BaseAPIResponse): + if ( + inspect.isclass(origin) + and issubclass(origin, BaseAPIResponse) + # we only want to actually return the custom BaseAPIResponse class if we're + # returning the raw response, or if we're not streaming SSE, as if we're streaming + # SSE then `cast_to` doesn't actively reflect the type we need to parse into + and (not stream or bool(response.request.headers.get(RAW_RESPONSE_HEADER))) + ): if not issubclass(origin, AsyncAPIResponse): raise TypeError(f"API Response types must subclass {AsyncAPIResponse}; Received {origin}") From adc22baf34672b29a5d15b2818b95394940261cd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 16 Jun 2025 14:19:29 +0000 Subject: [PATCH 264/399] chore(tests): add tests for httpx client instantiation & proxies --- tests/test_client.py | 46 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index d8a62f66..d4d803ae 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -32,6 +32,8 @@ DEFAULT_TIMEOUT, HTTPX_DEFAULT_TIMEOUT, BaseClient, + DefaultHttpxClient, + DefaultAsyncHttpxClient, make_request_options, ) from writerai.types.chat_chat_params import ChatChatParamsNonStreaming @@ -860,6 +862,28 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: assert response.http_request.headers.get("x-stainless-retry-count") == "42" + def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: + # Test that the proxy environment variables are set correctly + monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + + client = DefaultHttpxClient() + + mounts = tuple(client._mounts.items()) + assert len(mounts) == 1 + assert mounts[0][0].pattern == "https://" + + @pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning") + def test_default_client_creation(self) -> None: + # Ensure that the client can be initialized without any exceptions + DefaultHttpxClient( + verify=True, + cert=None, + trust_env=True, + http1=True, + http2=False, + limits=httpx.Limits(max_connections=100, max_keepalive_connections=20), + ) + @pytest.mark.respx(base_url=base_url) def test_follow_redirects(self, respx_mock: MockRouter) -> None: # Test that the default follow_redirects=True allows following redirects @@ -1755,6 +1779,28 @@ async def test_main() -> None: time.sleep(0.1) + async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: + # Test that the proxy environment variables are set correctly + monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + + client = DefaultAsyncHttpxClient() + + mounts = tuple(client._mounts.items()) + assert len(mounts) == 1 + assert mounts[0][0].pattern == "https://" + + @pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning") + async def test_default_client_creation(self) -> None: + # Ensure that the client can be initialized without any exceptions + DefaultAsyncHttpxClient( + verify=True, + cert=None, + trust_env=True, + http1=True, + http2=False, + limits=httpx.Limits(max_connections=100, max_keepalive_connections=20), + ) + @pytest.mark.respx(base_url=base_url) async def test_follow_redirects(self, respx_mock: MockRouter) -> None: # Test that the default follow_redirects=True allows following redirects From 4f1292f6037589e670c83672885381d547124afa Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 16 Jun 2025 18:58:52 +0000 Subject: [PATCH 265/399] chore(internal): update conftest.py --- tests/conftest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 5ba6ea6a..69cec86a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,5 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + from __future__ import annotations import os From e89a7a7ace2ff79dc0a0582388d1546eba13cbad Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 00:10:54 +0000 Subject: [PATCH 266/399] chore(ci): enable for pull requests --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f7d80a9a..f2abff2c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,10 @@ on: - 'integrated/**' - 'stl-preview-head/**' - 'stl-preview-base/**' + pull_request: + branches-ignore: + - 'stl-preview-head/**' + - 'stl-preview-base/**' jobs: lint: From e87ad2a10a2e21fae596c2d78e3dcbc935c807ac Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 14:52:17 +0000 Subject: [PATCH 267/399] chore(readme): update badges --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d6cb821f..91ec4cbd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Writer Python API library -[![PyPI version](https://img.shields.io/pypi/v/writer-sdk.svg)](https://pypi.org/project/writer-sdk/) +[![PyPI version]()](https://pypi.org/project/writer-sdk/) The Writer Python library provides convenient access to the Writer REST API from any Python 3.8+ application. The library includes type definitions for all request params and response fields, From 088a19bbf5cdf68d621316576ab2e8a9576fa05d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 21:11:27 +0000 Subject: [PATCH 268/399] fix(tests): fix: tests which call HTTP endpoints directly with the example parameters --- tests/test_client.py | 101 +++++-------------------------------------- 1 file changed, 12 insertions(+), 89 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index d4d803ae..52ea74bf 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -23,9 +23,7 @@ from writerai import Writer, AsyncWriter, APIResponseValidationError from writerai._types import Omit -from writerai._utils import maybe_transform from writerai._models import BaseModel, FinalRequestOptions -from writerai._constants import RAW_RESPONSE_HEADER from writerai._streaming import Stream, AsyncStream from writerai._exceptions import WriterError, APIStatusError, APITimeoutError, APIResponseValidationError from writerai._base_client import ( @@ -36,7 +34,6 @@ DefaultAsyncHttpxClient, make_request_options, ) -from writerai.types.chat_chat_params import ChatChatParamsNonStreaming from .utils import update_env @@ -725,60 +722,21 @@ def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str @mock.patch("writerai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: + def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: Writer) -> None: respx_mock.post("/v1/chat").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - self.client.post( - "/v1/chat", - body=cast( - object, - maybe_transform( - dict( - messages=[ - { - "content": "Write a haiku about programming", - "role": "user", - } - ], - model="palmyra-x5", - ), - ChatChatParamsNonStreaming, - ), - ), - cast_to=httpx.Response, - options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, - ) + client.chat.with_streaming_response.chat(messages=[{"role": "user"}], model="model").__enter__() assert _get_open_connections(self.client) == 0 @mock.patch("writerai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: + def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: Writer) -> None: respx_mock.post("/v1/chat").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - self.client.post( - "/v1/chat", - body=cast( - object, - maybe_transform( - dict( - messages=[ - { - "content": "Write a haiku about programming", - "role": "user", - } - ], - model="palmyra-x5", - ), - ChatChatParamsNonStreaming, - ), - ), - cast_to=httpx.Response, - options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, - ) - + client.chat.with_streaming_response.chat(messages=[{"role": "user"}], model="model").__enter__() assert _get_open_connections(self.client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -1594,60 +1552,25 @@ async def test_parse_retry_after_header(self, remaining_retries: int, retry_afte @mock.patch("writerai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: + async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, async_client: AsyncWriter) -> None: respx_mock.post("/v1/chat").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - await self.client.post( - "/v1/chat", - body=cast( - object, - maybe_transform( - dict( - messages=[ - { - "content": "Write a haiku about programming", - "role": "user", - } - ], - model="palmyra-x5", - ), - ChatChatParamsNonStreaming, - ), - ), - cast_to=httpx.Response, - options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, - ) + await async_client.chat.with_streaming_response.chat( + messages=[{"role": "user"}], model="model" + ).__aenter__() assert _get_open_connections(self.client) == 0 @mock.patch("writerai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: + async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, async_client: AsyncWriter) -> None: respx_mock.post("/v1/chat").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - await self.client.post( - "/v1/chat", - body=cast( - object, - maybe_transform( - dict( - messages=[ - { - "content": "Write a haiku about programming", - "role": "user", - } - ], - model="palmyra-x5", - ), - ChatChatParamsNonStreaming, - ), - ), - cast_to=httpx.Response, - options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, - ) - + await async_client.chat.with_streaming_response.chat( + messages=[{"role": "user"}], model="model" + ).__aenter__() assert _get_open_connections(self.client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) From 44d40bcbe993e161959930ffd13c94942c21897e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 12:41:10 +0000 Subject: [PATCH 269/399] docs(client): fix httpx.Timeout documentation reference --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 91ec4cbd..12eef428 100644 --- a/README.md +++ b/README.md @@ -294,7 +294,7 @@ client.with_options(max_retries=5).chat.chat( ### Timeouts By default requests time out after 3 minutes. You can configure this with a `timeout` option, -which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/#fine-tuning-the-configuration) object: +which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/timeouts/#fine-tuning-the-configuration) object: ```python from writerai import Writer From 9289fc09a07ccd29f8aa794f95a889806ae71dce Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Jun 2025 18:01:52 +0000 Subject: [PATCH 270/399] feat(client): add support for aiohttp --- README.md | 40 +++++++++++++++++ pyproject.toml | 2 + requirements-dev.lock | 27 ++++++++++++ requirements.lock | 27 ++++++++++++ src/writerai/__init__.py | 3 +- src/writerai/_base_client.py | 22 ++++++++++ .../api_resources/applications/test_graphs.py | 4 +- tests/api_resources/applications/test_jobs.py | 4 +- tests/api_resources/test_applications.py | 4 +- tests/api_resources/test_chat.py | 4 +- tests/api_resources/test_completions.py | 4 +- tests/api_resources/test_files.py | 4 +- tests/api_resources/test_graphs.py | 4 +- tests/api_resources/test_models.py | 4 +- tests/api_resources/test_tools.py | 4 +- tests/api_resources/test_translation.py | 4 +- tests/api_resources/test_vision.py | 4 +- tests/api_resources/tools/test_comprehend.py | 4 +- tests/conftest.py | 43 ++++++++++++++++--- 19 files changed, 193 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 12eef428..bd2e3efc 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,46 @@ asyncio.run(main()) Functionality between the synchronous and asynchronous clients is otherwise identical. +### With aiohttp + +By default, the async client uses `httpx` for HTTP requests. However, for improved concurrency performance you may also use `aiohttp` as the HTTP backend. + +You can enable this by installing `aiohttp`: + +```sh +# install from PyPI +pip install writer-sdk[aiohttp] +``` + +Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: + +```python +import os +import asyncio +from writerai import DefaultAioHttpClient +from writerai import AsyncWriter + + +async def main() -> None: + async with AsyncWriter( + api_key=os.environ.get("WRITER_API_KEY"), # This is the default and can be omitted + http_client=DefaultAioHttpClient(), + ) as client: + chat_completion = await client.chat.chat( + messages=[ + { + "content": "Write a haiku about programming", + "role": "user", + } + ], + model="palmyra-x5", + ) + print(chat_completion.id) + + +asyncio.run(main()) +``` + ## Streaming responses We provide support for streaming responses using Server Side Events (SSE). diff --git a/pyproject.toml b/pyproject.toml index 2016bbb6..b42e2fc7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,8 @@ classifiers = [ Homepage = "https://github.com/writer/writer-python" Repository = "https://github.com/writer/writer-python" +[project.optional-dependencies] +aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.6"] [tool.rye] managed = true diff --git a/requirements-dev.lock b/requirements-dev.lock index 3601bc33..40b4d9c7 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -10,6 +10,13 @@ # universal: false -e file:. +aiohappyeyeballs==2.6.1 + # via aiohttp +aiohttp==3.12.8 + # via httpx-aiohttp + # via writer-sdk +aiosignal==1.3.2 + # via aiohttp annotated-types==0.6.0 # via pydantic anyio==4.4.0 @@ -17,6 +24,10 @@ anyio==4.4.0 # via writer-sdk argcomplete==3.1.2 # via nox +async-timeout==5.0.1 + # via aiohttp +attrs==25.3.0 + # via aiohttp certifi==2023.7.22 # via httpcore # via httpx @@ -34,16 +45,23 @@ execnet==2.1.1 # via pytest-xdist filelock==3.12.4 # via virtualenv +frozenlist==1.6.2 + # via aiohttp + # via aiosignal h11==0.14.0 # via httpcore httpcore==1.0.2 # via httpx httpx==0.28.1 + # via httpx-aiohttp # via respx # via writer-sdk +httpx-aiohttp==0.1.6 + # via writer-sdk idna==3.4 # via anyio # via httpx + # via yarl importlib-metadata==7.0.0 iniconfig==2.0.0 # via pytest @@ -51,6 +69,9 @@ markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py +multidict==6.4.4 + # via aiohttp + # via yarl mypy==1.14.1 mypy-extensions==1.0.0 # via mypy @@ -65,6 +86,9 @@ platformdirs==3.11.0 # via virtualenv pluggy==1.5.0 # via pytest +propcache==0.3.1 + # via aiohttp + # via yarl pydantic==2.10.3 # via writer-sdk pydantic-core==2.27.1 @@ -97,6 +121,7 @@ tomli==2.0.2 # via pytest typing-extensions==4.12.2 # via anyio + # via multidict # via mypy # via pydantic # via pydantic-core @@ -104,5 +129,7 @@ typing-extensions==4.12.2 # via writer-sdk virtualenv==20.24.5 # via nox +yarl==1.20.0 + # via aiohttp zipp==3.17.0 # via importlib-metadata diff --git a/requirements.lock b/requirements.lock index 8bd96abb..32597372 100644 --- a/requirements.lock +++ b/requirements.lock @@ -10,11 +10,22 @@ # universal: false -e file:. +aiohappyeyeballs==2.6.1 + # via aiohttp +aiohttp==3.12.8 + # via httpx-aiohttp + # via writer-sdk +aiosignal==1.3.2 + # via aiohttp annotated-types==0.6.0 # via pydantic anyio==4.4.0 # via httpx # via writer-sdk +async-timeout==5.0.1 + # via aiohttp +attrs==25.3.0 + # via aiohttp certifi==2023.7.22 # via httpcore # via httpx @@ -22,15 +33,28 @@ distro==1.8.0 # via writer-sdk exceptiongroup==1.2.2 # via anyio +frozenlist==1.6.2 + # via aiohttp + # via aiosignal h11==0.14.0 # via httpcore httpcore==1.0.2 # via httpx httpx==0.28.1 + # via httpx-aiohttp + # via writer-sdk +httpx-aiohttp==0.1.6 # via writer-sdk idna==3.4 # via anyio # via httpx + # via yarl +multidict==6.4.4 + # via aiohttp + # via yarl +propcache==0.3.1 + # via aiohttp + # via yarl pydantic==2.10.3 # via writer-sdk pydantic-core==2.27.1 @@ -40,6 +64,9 @@ sniffio==1.3.0 # via writer-sdk typing-extensions==4.12.2 # via anyio + # via multidict # via pydantic # via pydantic-core # via writer-sdk +yarl==1.20.0 + # via aiohttp diff --git a/src/writerai/__init__.py b/src/writerai/__init__.py index 289aabb4..c63a1e2c 100644 --- a/src/writerai/__init__.py +++ b/src/writerai/__init__.py @@ -26,7 +26,7 @@ UnprocessableEntityError, APIResponseValidationError, ) -from ._base_client import DefaultHttpxClient, DefaultAsyncHttpxClient +from ._base_client import DefaultHttpxClient, DefaultAioHttpClient, DefaultAsyncHttpxClient from ._utils._logs import setup_logging as _setup_logging __all__ = [ @@ -68,6 +68,7 @@ "DEFAULT_CONNECTION_LIMITS", "DefaultHttpxClient", "DefaultAsyncHttpxClient", + "DefaultAioHttpClient", ] if not _t.TYPE_CHECKING: diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index 863e7144..1f02fe51 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -1289,6 +1289,24 @@ def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) +try: + import httpx_aiohttp +except ImportError: + + class _DefaultAioHttpClient(httpx.AsyncClient): + def __init__(self, **_kwargs: Any) -> None: + raise RuntimeError("To use the aiohttp client you must have installed the package with the `aiohttp` extra") +else: + + class _DefaultAioHttpClient(httpx_aiohttp.HttpxAiohttpClient): # type: ignore + def __init__(self, **kwargs: Any) -> None: + kwargs.setdefault("timeout", DEFAULT_TIMEOUT) + kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) + kwargs.setdefault("follow_redirects", True) + + super().__init__(**kwargs) + + if TYPE_CHECKING: DefaultAsyncHttpxClient = httpx.AsyncClient """An alias to `httpx.AsyncClient` that provides the same defaults that this SDK @@ -1297,8 +1315,12 @@ def __init__(self, **kwargs: Any) -> None: This is useful because overriding the `http_client` with your own instance of `httpx.AsyncClient` will result in httpx's defaults being used, not ours. """ + + DefaultAioHttpClient = httpx.AsyncClient + """An alias to `httpx.AsyncClient` that changes the default HTTP transport to `aiohttp`.""" else: DefaultAsyncHttpxClient = _DefaultAsyncHttpxClient + DefaultAioHttpClient = _DefaultAioHttpClient class AsyncHttpxClientWrapper(DefaultAsyncHttpxClient): diff --git a/tests/api_resources/applications/test_graphs.py b/tests/api_resources/applications/test_graphs.py index 3228b9d1..e7c2c30e 100644 --- a/tests/api_resources/applications/test_graphs.py +++ b/tests/api_resources/applications/test_graphs.py @@ -99,7 +99,9 @@ def test_path_params_list(self, client: Writer) -> None: class TestAsyncGraphs: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_update(self, async_client: AsyncWriter) -> None: diff --git a/tests/api_resources/applications/test_jobs.py b/tests/api_resources/applications/test_jobs.py index 57eba30f..ff13b328 100644 --- a/tests/api_resources/applications/test_jobs.py +++ b/tests/api_resources/applications/test_jobs.py @@ -210,7 +210,9 @@ def test_path_params_retry(self, client: Writer) -> None: class TestAsyncJobs: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_create(self, async_client: AsyncWriter) -> None: diff --git a/tests/api_resources/test_applications.py b/tests/api_resources/test_applications.py index 86678535..b4813d58 100644 --- a/tests/api_resources/test_applications.py +++ b/tests/api_resources/test_applications.py @@ -239,7 +239,9 @@ def test_path_params_generate_content_overload_2(self, client: Writer) -> None: class TestAsyncApplications: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_retrieve(self, async_client: AsyncWriter) -> None: diff --git a/tests/api_resources/test_chat.py b/tests/api_resources/test_chat.py index d04446a9..f47c2ade 100644 --- a/tests/api_resources/test_chat.py +++ b/tests/api_resources/test_chat.py @@ -231,7 +231,9 @@ def test_streaming_response_chat_overload_2(self, client: Writer) -> None: class TestAsyncChat: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_chat_overload_1(self, async_client: AsyncWriter) -> None: diff --git a/tests/api_resources/test_completions.py b/tests/api_resources/test_completions.py index e39a6d99..246bdfc9 100644 --- a/tests/api_resources/test_completions.py +++ b/tests/api_resources/test_completions.py @@ -119,7 +119,9 @@ def test_streaming_response_create_overload_2(self, client: Writer) -> None: class TestAsyncCompletions: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_create_overload_1(self, async_client: AsyncWriter) -> None: diff --git a/tests/api_resources/test_files.py b/tests/api_resources/test_files.py index 27622c6b..142987d1 100644 --- a/tests/api_resources/test_files.py +++ b/tests/api_resources/test_files.py @@ -268,7 +268,9 @@ def test_streaming_response_upload(self, client: Writer) -> None: class TestAsyncFiles: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_retrieve(self, async_client: AsyncWriter) -> None: diff --git a/tests/api_resources/test_graphs.py b/tests/api_resources/test_graphs.py index 22c7b21b..15ae7a62 100644 --- a/tests/api_resources/test_graphs.py +++ b/tests/api_resources/test_graphs.py @@ -399,7 +399,9 @@ def test_path_params_remove_file_from_graph(self, client: Writer) -> None: class TestAsyncGraphs: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_create(self, async_client: AsyncWriter) -> None: diff --git a/tests/api_resources/test_models.py b/tests/api_resources/test_models.py index 8ed984dc..3ad1f08f 100644 --- a/tests/api_resources/test_models.py +++ b/tests/api_resources/test_models.py @@ -44,7 +44,9 @@ def test_streaming_response_list(self, client: Writer) -> None: class TestAsyncModels: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_list(self, async_client: AsyncWriter) -> None: diff --git a/tests/api_resources/test_tools.py b/tests/api_resources/test_tools.py index 47026f82..bbc2a7db 100644 --- a/tests/api_resources/test_tools.py +++ b/tests/api_resources/test_tools.py @@ -130,7 +130,9 @@ def test_path_params_parse_pdf(self, client: Writer) -> None: class TestAsyncTools: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_ai_detect(self, async_client: AsyncWriter) -> None: diff --git a/tests/api_resources/test_translation.py b/tests/api_resources/test_translation.py index caa3296f..e7a0babc 100644 --- a/tests/api_resources/test_translation.py +++ b/tests/api_resources/test_translation.py @@ -68,7 +68,9 @@ def test_streaming_response_translate(self, client: Writer) -> None: class TestAsyncTranslation: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_translate(self, async_client: AsyncWriter) -> None: diff --git a/tests/api_resources/test_vision.py b/tests/api_resources/test_vision.py index 20f5d72f..dfca8433 100644 --- a/tests/api_resources/test_vision.py +++ b/tests/api_resources/test_vision.py @@ -83,7 +83,9 @@ def test_streaming_response_analyze(self, client: Writer) -> None: class TestAsyncVision: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_analyze(self, async_client: AsyncWriter) -> None: diff --git a/tests/api_resources/tools/test_comprehend.py b/tests/api_resources/tools/test_comprehend.py index bf026b75..6f7efe39 100644 --- a/tests/api_resources/tools/test_comprehend.py +++ b/tests/api_resources/tools/test_comprehend.py @@ -53,7 +53,9 @@ def test_streaming_response_medical(self, client: Writer) -> None: class TestAsyncComprehend: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_medical(self, async_client: AsyncWriter) -> None: diff --git a/tests/conftest.py b/tests/conftest.py index 69cec86a..ec6f42e1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,10 +6,12 @@ import logging from typing import TYPE_CHECKING, Iterator, AsyncIterator +import httpx import pytest from pytest_asyncio import is_async_test -from writerai import Writer, AsyncWriter +from writerai import Writer, AsyncWriter, DefaultAioHttpClient +from writerai._utils import is_dict if TYPE_CHECKING: from _pytest.fixtures import FixtureRequest # pyright: ignore[reportPrivateImportUsage] @@ -27,6 +29,19 @@ def pytest_collection_modifyitems(items: list[pytest.Function]) -> None: for async_test in pytest_asyncio_tests: async_test.add_marker(session_scope_marker, append=False) + # We skip tests that use both the aiohttp client and respx_mock as respx_mock + # doesn't support custom transports. + for item in items: + if "async_client" not in item.fixturenames or "respx_mock" not in item.fixturenames: + continue + + if not hasattr(item, "callspec"): + continue + + async_client_param = item.callspec.params.get("async_client") + if is_dict(async_client_param) and async_client_param.get("http_client") == "aiohttp": + item.add_marker(pytest.mark.skip(reason="aiohttp client is not compatible with respx_mock")) + base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -45,9 +60,25 @@ def client(request: FixtureRequest) -> Iterator[Writer]: @pytest.fixture(scope="session") async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncWriter]: - strict = getattr(request, "param", True) - if not isinstance(strict, bool): - raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") - - async with AsyncWriter(base_url=base_url, api_key=api_key, _strict_response_validation=strict) as client: + param = getattr(request, "param", True) + + # defaults + strict = True + http_client: None | httpx.AsyncClient = None + + if isinstance(param, bool): + strict = param + elif is_dict(param): + strict = param.get("strict", True) + assert isinstance(strict, bool) + + http_client_type = param.get("http_client", "httpx") + if http_client_type == "aiohttp": + http_client = DefaultAioHttpClient() + else: + raise TypeError(f"Unexpected fixture parameter type {type(param)}, expected bool or dict") + + async with AsyncWriter( + base_url=base_url, api_key=api_key, _strict_response_validation=strict, http_client=http_client + ) as client: yield client From 16ddc748973f17e374a30d154d5db447a84b1572 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Jun 2025 18:49:59 +0000 Subject: [PATCH 271/399] chore(tests): skip some failing tests on the latest python versions --- tests/test_client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index 52ea74bf..0171405d 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -192,6 +192,7 @@ def test_copy_signature(self) -> None: copy_param = copy_signature.parameters.get(name) assert copy_param is not None, f"copy() signature is missing the {name} param" + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12") def test_copy_build_request(self) -> None: options = FinalRequestOptions(method="get", url="/foo") @@ -1005,6 +1006,7 @@ def test_copy_signature(self) -> None: copy_param = copy_signature.parameters.get(name) assert copy_param is not None, f"copy() signature is missing the {name} param" + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12") def test_copy_build_request(self) -> None: options = FinalRequestOptions(method="get", url="/foo") From b5687e8e0d1711247ca85b22e51a06b2e092f0bc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 26 Jun 2025 14:05:56 +0000 Subject: [PATCH 272/399] =?UTF-8?q?fix(ci):=20release-doctor=20=E2=80=94?= =?UTF-8?q?=20report=20correct=20token=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/check-release-environment | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/check-release-environment b/bin/check-release-environment index fe3283dd..b845b0f4 100644 --- a/bin/check-release-environment +++ b/bin/check-release-environment @@ -3,7 +3,7 @@ errors=() if [ -z "${PYPI_TOKEN}" ]; then - errors+=("The WRITER_PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.") + errors+=("The PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.") fi lenErrors=${#errors[@]} From f7d77a8ee41fa0d060306f6f92f33519021f9663 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 27 Jun 2025 22:40:18 +0000 Subject: [PATCH 273/399] chore(ci): only run for pushes and fork pull requests --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f2abff2c..eb9b286d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,7 @@ jobs: timeout-minutes: 10 name: lint runs-on: ${{ github.repository == 'stainless-sdks/writer-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@v4 @@ -42,6 +43,7 @@ jobs: contents: read id-token: write runs-on: depot-ubuntu-24.04 + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@v4 @@ -62,6 +64,7 @@ jobs: timeout-minutes: 10 name: test runs-on: ${{ github.repository == 'stainless-sdks/writer-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@v4 From 020bbd197a802701beae8a8d1bde651a3a38d38d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 29 Jun 2025 06:19:10 +0000 Subject: [PATCH 274/399] fix(ci): correct conditional --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb9b286d..754c6a36 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,14 +36,13 @@ jobs: run: ./scripts/lint upload: - if: github.repository == 'stainless-sdks/writer-python' + if: github.repository == 'stainless-sdks/writer-python' && (github.event_name == 'push' || github.event.pull_request.head.repo.fork) timeout-minutes: 10 name: upload permissions: contents: read id-token: write runs-on: depot-ubuntu-24.04 - if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@v4 From 6e18889500995c6b459797e5a7c0a4b7a58f3e44 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 1 Jul 2025 23:34:44 +0000 Subject: [PATCH 275/399] chore(ci): change upload type --- .github/workflows/ci.yml | 18 ++++++++++++++++-- scripts/utils/upload-artifact.sh | 12 +++++++----- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 754c6a36..beb8dd8c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,10 +35,10 @@ jobs: - name: Run lints run: ./scripts/lint - upload: + build: if: github.repository == 'stainless-sdks/writer-python' && (github.event_name == 'push' || github.event.pull_request.head.repo.fork) timeout-minutes: 10 - name: upload + name: build permissions: contents: read id-token: write @@ -46,6 +46,20 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Install Rye + run: | + curl -sSf https://rye.astral.sh/get | bash + echo "$HOME/.rye/shims" >> $GITHUB_PATH + env: + RYE_VERSION: '0.44.0' + RYE_INSTALL_OPTION: '--yes' + + - name: Install dependencies + run: rye sync --all-features + + - name: Run build + run: rye build + - name: Get GitHub OIDC Token id: github-oidc uses: actions/github-script@v6 diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh index 4e270259..af535aa5 100755 --- a/scripts/utils/upload-artifact.sh +++ b/scripts/utils/upload-artifact.sh @@ -1,7 +1,9 @@ #!/usr/bin/env bash set -exuo pipefail -RESPONSE=$(curl -X POST "$URL" \ +FILENAME=$(basename dist/*.whl) + +RESPONSE=$(curl -X POST "$URL?filename=$FILENAME" \ -H "Authorization: Bearer $AUTH" \ -H "Content-Type: application/json") @@ -12,13 +14,13 @@ if [[ "$SIGNED_URL" == "null" ]]; then exit 1 fi -UPLOAD_RESPONSE=$(tar -cz . | curl -v -X PUT \ - -H "Content-Type: application/gzip" \ - --data-binary @- "$SIGNED_URL" 2>&1) +UPLOAD_RESPONSE=$(curl -v -X PUT \ + -H "Content-Type: binary/octet-stream" \ + --data-binary "@dist/$FILENAME" "$SIGNED_URL" 2>&1) if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then echo -e "\033[32mUploaded build to Stainless storage.\033[0m" - echo -e "\033[32mInstallation: pip install 'https://pkg.stainless.com/s/writer-python/$SHA'\033[0m" + echo -e "\033[32mInstallation: pip install 'https://pkg.stainless.com/s/writer-python/$SHA/$FILENAME'\033[0m" else echo -e "\033[31mFailed to upload artifact.\033[0m" exit 1 From 86074e961edebc2c694b04e5134ae801fbd16352 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 15:56:44 +0000 Subject: [PATCH 276/399] chore(internal): codegen related update --- requirements-dev.lock | 2 +- requirements.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-dev.lock b/requirements-dev.lock index 40b4d9c7..cb70959c 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -56,7 +56,7 @@ httpx==0.28.1 # via httpx-aiohttp # via respx # via writer-sdk -httpx-aiohttp==0.1.6 +httpx-aiohttp==0.1.8 # via writer-sdk idna==3.4 # via anyio diff --git a/requirements.lock b/requirements.lock index 32597372..11492e10 100644 --- a/requirements.lock +++ b/requirements.lock @@ -43,7 +43,7 @@ httpcore==1.0.2 httpx==0.28.1 # via httpx-aiohttp # via writer-sdk -httpx-aiohttp==0.1.6 +httpx-aiohttp==0.1.8 # via writer-sdk idna==3.4 # via anyio From 4a48362b93728d4119ff3572d71c6d23d876b43e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 8 Jul 2025 14:25:57 +0000 Subject: [PATCH 277/399] chore(internal): bump pinned h11 dep --- requirements-dev.lock | 4 ++-- requirements.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements-dev.lock b/requirements-dev.lock index cb70959c..6b34ee8e 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -48,9 +48,9 @@ filelock==3.12.4 frozenlist==1.6.2 # via aiohttp # via aiosignal -h11==0.14.0 +h11==0.16.0 # via httpcore -httpcore==1.0.2 +httpcore==1.0.9 # via httpx httpx==0.28.1 # via httpx-aiohttp diff --git a/requirements.lock b/requirements.lock index 11492e10..65ee24ec 100644 --- a/requirements.lock +++ b/requirements.lock @@ -36,9 +36,9 @@ exceptiongroup==1.2.2 frozenlist==1.6.2 # via aiohttp # via aiosignal -h11==0.14.0 +h11==0.16.0 # via httpcore -httpcore==1.0.2 +httpcore==1.0.9 # via httpx httpx==0.28.1 # via httpx-aiohttp From 687bcd3a925736ed908ac16001d31740bfd184e5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 8 Jul 2025 15:27:35 +0000 Subject: [PATCH 278/399] chore(package): mark python 3.13 as supported --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index b42e2fc7..f61fe6cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: MacOS", From 80e72291bcc0b269418b9b745bfe370ea6f1fa70 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 9 Jul 2025 10:16:46 +0000 Subject: [PATCH 279/399] fix(parsing): correctly handle nested discriminated unions --- src/writerai/_models.py | 13 +++++++----- tests/test_models.py | 45 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/src/writerai/_models.py b/src/writerai/_models.py index 4f214980..528d5680 100644 --- a/src/writerai/_models.py +++ b/src/writerai/_models.py @@ -2,9 +2,10 @@ import os import inspect -from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, cast +from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast from datetime import date, datetime from typing_extensions import ( + List, Unpack, Literal, ClassVar, @@ -366,7 +367,7 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object: if type_ is None: raise RuntimeError(f"Unexpected field type is None for {key}") - return construct_type(value=value, type_=type_) + return construct_type(value=value, type_=type_, metadata=getattr(field, "metadata", None)) def is_basemodel(type_: type) -> bool: @@ -420,7 +421,7 @@ def construct_type_unchecked(*, value: object, type_: type[_T]) -> _T: return cast(_T, construct_type(value=value, type_=type_)) -def construct_type(*, value: object, type_: object) -> object: +def construct_type(*, value: object, type_: object, metadata: Optional[List[Any]] = None) -> object: """Loose coercion to the expected type with construction of nested values. If the given value does not match the expected type then it is returned as-is. @@ -438,8 +439,10 @@ def construct_type(*, value: object, type_: object) -> object: type_ = type_.__value__ # type: ignore[unreachable] # unwrap `Annotated[T, ...]` -> `T` - if is_annotated_type(type_): - meta: tuple[Any, ...] = get_args(type_)[1:] + if metadata is not None: + meta: tuple[Any, ...] = tuple(metadata) + elif is_annotated_type(type_): + meta = get_args(type_)[1:] type_ = extract_type_arg(type_, 0) else: meta = tuple() diff --git a/tests/test_models.py b/tests/test_models.py index 93d48eea..65019f23 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -889,3 +889,48 @@ class ModelB(BaseModel): ) assert isinstance(m, ModelB) + + +def test_nested_discriminated_union() -> None: + class InnerType1(BaseModel): + type: Literal["type_1"] + + class InnerModel(BaseModel): + inner_value: str + + class InnerType2(BaseModel): + type: Literal["type_2"] + some_inner_model: InnerModel + + class Type1(BaseModel): + base_type: Literal["base_type_1"] + value: Annotated[ + Union[ + InnerType1, + InnerType2, + ], + PropertyInfo(discriminator="type"), + ] + + class Type2(BaseModel): + base_type: Literal["base_type_2"] + + T = Annotated[ + Union[ + Type1, + Type2, + ], + PropertyInfo(discriminator="base_type"), + ] + + model = construct_type( + type_=T, + value={ + "base_type": "base_type_1", + "value": { + "type": "type_2", + }, + }, + ) + assert isinstance(model, Type1) + assert isinstance(model.value, InnerType2) From 2318762ef12518ce4b5df8b33b3053fb85cb29a8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 10 Jul 2025 17:46:33 +0000 Subject: [PATCH 280/399] chore(readme): fix version rendering on pypi --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bd2e3efc..5a11d139 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Writer Python API library -[![PyPI version]()](https://pypi.org/project/writer-sdk/) + +[![PyPI version](https://img.shields.io/pypi/v/writer-sdk.svg?label=pypi%20(stable))](https://pypi.org/project/writer-sdk/) The Writer Python library provides convenient access to the Writer REST API from any Python 3.8+ application. The library includes type definitions for all request params and response fields, From 07020c228eaca794454a46109223e204f5c5a631 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 11 Jul 2025 12:14:20 +0000 Subject: [PATCH 281/399] fix(client): don't send Content-Type header on GET requests --- pyproject.toml | 2 +- src/writerai/_base_client.py | 11 +++++++++-- tests/test_client.py | 4 ++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f61fe6cf..9bc5014b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ Homepage = "https://github.com/writer/writer-python" Repository = "https://github.com/writer/writer-python" [project.optional-dependencies] -aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.6"] +aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.8"] [tool.rye] managed = true diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index 1f02fe51..cc32903b 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -529,6 +529,15 @@ def _build_request( # work around https://github.com/encode/httpx/discussions/2880 kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")} + is_body_allowed = options.method.lower() != "get" + + if is_body_allowed: + kwargs["json"] = json_data if is_given(json_data) else None + kwargs["files"] = files + else: + headers.pop("Content-Type", None) + kwargs.pop("data", None) + # TODO: report this error to httpx return self._client.build_request( # pyright: ignore[reportUnknownMemberType] headers=headers, @@ -540,8 +549,6 @@ def _build_request( # so that passing a `TypedDict` doesn't cause an error. # https://github.com/microsoft/pyright/issues/3526#event-6715453066 params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None, - json=json_data if is_given(json_data) else None, - files=files, **kwargs, ) diff --git a/tests/test_client.py b/tests/test_client.py index 0171405d..03dbad51 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -463,7 +463,7 @@ def test_request_extra_query(self) -> None: def test_multipart_repeating_array(self, client: Writer) -> None: request = client._build_request( FinalRequestOptions.construct( - method="get", + method="post", url="/foo", headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"}, json_data={"array": ["foo", "bar"]}, @@ -1279,7 +1279,7 @@ def test_request_extra_query(self) -> None: def test_multipart_repeating_array(self, async_client: AsyncWriter) -> None: request = async_client._build_request( FinalRequestOptions.construct( - method="get", + method="post", url="/foo", headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"}, json_data={"array": ["foo", "bar"]}, From 0d9442f3df7c0bafb9000ced67b1fcafaba22117 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 17:31:32 +0000 Subject: [PATCH 282/399] feat: clean up environment call outs --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 5a11d139..3ecfe7d9 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,6 @@ pip install writer-sdk[aiohttp] Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: ```python -import os import asyncio from writerai import DefaultAioHttpClient from writerai import AsyncWriter @@ -103,7 +102,7 @@ from writerai import AsyncWriter async def main() -> None: async with AsyncWriter( - api_key=os.environ.get("WRITER_API_KEY"), # This is the default and can be omitted + api_key="My API Key", http_client=DefaultAioHttpClient(), ) as client: chat_completion = await client.chat.chat( From 4dde623b28b6743509f6cc0db76d54fe9420c743 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 16 Jul 2025 22:01:42 +0000 Subject: [PATCH 283/399] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index bfc26f9c..89d8ff81 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.2.0" + ".": "2.2.1" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 9bc5014b..997f6913 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "2.2.0" +version = "2.2.1" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index 3033a06d..4fba2a61 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "2.2.0" # x-release-please-version +__version__ = "2.2.1" # x-release-please-version From 45ea1f7592734bc8c74b11363c65ef938b4e6fb6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 21 Jul 2025 12:28:55 +0000 Subject: [PATCH 284/399] fix(parsing): ignore empty metadata --- src/writerai/_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/writerai/_models.py b/src/writerai/_models.py index 528d5680..ffcbf67b 100644 --- a/src/writerai/_models.py +++ b/src/writerai/_models.py @@ -439,7 +439,7 @@ def construct_type(*, value: object, type_: object, metadata: Optional[List[Any] type_ = type_.__value__ # type: ignore[unreachable] # unwrap `Annotated[T, ...]` -> `T` - if metadata is not None: + if metadata is not None and len(metadata) > 0: meta: tuple[Any, ...] = tuple(metadata) elif is_annotated_type(type_): meta = get_args(type_)[1:] From 55fce92b2d8aae0b231cf3b127eb235921a852eb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 21 Jul 2025 17:35:36 +0000 Subject: [PATCH 285/399] docs(api): updates to API spec --- .stats.yml | 4 +- api.md | 6 +- src/writerai/resources/applications/graphs.py | 4 +- src/writerai/resources/chat.py | 162 ++++++++++++------ src/writerai/resources/graphs.py | 37 ++-- src/writerai/resources/translation.py | 20 +-- src/writerai/types/__init__.py | 3 +- .../application_generate_content_params.py | 4 +- .../types/applications/job_create_params.py | 4 +- src/writerai/types/chat_chat_params.py | 29 ++-- src/writerai/types/graph.py | 45 ----- src/writerai/types/graph_create_response.py | 32 +++- src/writerai/types/graph_list_response.py | 76 ++++++++ src/writerai/types/graph_retrieve_response.py | 76 ++++++++ src/writerai/types/graph_update_params.py | 23 ++- src/writerai/types/graph_update_response.py | 32 +++- .../types/shared/tool_choice_json_object.py | 4 + src/writerai/types/shared/tool_param.py | 8 +- .../shared_params/tool_choice_json_object.py | 4 + .../types/shared_params/tool_param.py | 8 +- .../types/translation_translate_params.py | 10 +- tests/api_resources/test_graphs.py | 45 +++-- 22 files changed, 466 insertions(+), 170 deletions(-) delete mode 100644 src/writerai/types/graph.py create mode 100644 src/writerai/types/graph_list_response.py create mode 100644 src/writerai/types/graph_retrieve_response.py diff --git a/.stats.yml b/.stats.yml index 38d9f82d..40491d42 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 32 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-fab7a71148b6f413a4425ca9f2ce3d42557b65d35ab28c6f64daa7fce6d0ffe2.yml -openapi_spec_hash: 0ead6944545bc40172176e15cc704633 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-994b51f2c7c776c2cd3d4e3c6900cc6291da87296cea27921ec709a459a41034.yml +openapi_spec_hash: 1fdc6bb31a5464cebb4c579370764907 config_hash: c0c9f57ab19252f82cf765939edc61de diff --git a/api.md b/api.md index 5ebeffe3..aca1096e 100644 --- a/api.md +++ b/api.md @@ -123,7 +123,9 @@ from writerai.types import ( Question, QuestionResponseChunk, GraphCreateResponse, + GraphRetrieveResponse, GraphUpdateResponse, + GraphListResponse, GraphDeleteResponse, GraphRemoveFileFromGraphResponse, ) @@ -132,9 +134,9 @@ from writerai.types import ( Methods: - client.graphs.create(\*\*params) -> GraphCreateResponse -- client.graphs.retrieve(graph_id) -> Graph +- client.graphs.retrieve(graph_id) -> GraphRetrieveResponse - client.graphs.update(graph_id, \*\*params) -> GraphUpdateResponse -- client.graphs.list(\*\*params) -> SyncCursorPage[Graph] +- client.graphs.list(\*\*params) -> SyncCursorPage[GraphListResponse] - client.graphs.delete(graph_id) -> GraphDeleteResponse - client.graphs.add_file_to_graph(graph_id, \*\*params) -> File - client.graphs.question(\*\*params) -> Question diff --git a/src/writerai/resources/applications/graphs.py b/src/writerai/resources/applications/graphs.py index e87e147a..9ab86c4e 100644 --- a/src/writerai/resources/applications/graphs.py +++ b/src/writerai/resources/applications/graphs.py @@ -56,7 +56,7 @@ def update( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ApplicationGraphsResponse: """ - Updates the Knowledge Graphs listed and associates them with the no-code agent. + Updates the list of Knowledge Graphs associated with a no-code chat agent. Args: graph_ids: A list of Knowledge Graph IDs to associate with the application. Note that this @@ -150,7 +150,7 @@ async def update( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ApplicationGraphsResponse: """ - Updates the Knowledge Graphs listed and associates them with the no-code agent. + Updates the list of Knowledge Graphs associated with a no-code chat agent. Args: graph_ids: A list of Knowledge Graph IDs to associate with the application. Note that this diff --git a/src/writerai/resources/chat.py b/src/writerai/resources/chat.py index f388d1e8..85ee1464 100644 --- a/src/writerai/resources/chat.py +++ b/src/writerai/resources/chat.py @@ -75,7 +75,7 @@ def chat( The response shown below is for non-streaming. To learn about streaming responses, see the - [chat completion guide](https://dev.writer.com/api-guides/chat-completion). + [chat completion guide](https://dev.writer.com/home/chat-completion). Args: messages: An array of message objects that form the conversation history or context for @@ -88,8 +88,10 @@ def chat( logprobs: Specifies whether to return log probabilities of the output tokens. max_tokens: Defines the maximum number of tokens (words and characters) that the model can - generate in the response. The default value is set to 16, but it can be adjusted - to allow for longer or shorter responses as needed. + generate in the response. This can be adjusted to allow for longer or shorter + responses as needed. The maximum value varies by model. See the + [models overview](/home/models) for more information about the maximum number of + tokens for each model. n: Specifies the number of completions (responses) to generate from the model in a single request. This parameter allows for generating multiple responses, @@ -116,17 +118,26 @@ def chat( temperature results in more varied and less predictable text, while a lower temperature produces more deterministic and conservative outputs. - tool_choice: Configure how the model will call functions: `auto` will allow the model to - automatically choose the best tool, `none` disables tool calling. You can also - pass a specific previously defined function. + tool_choice: + Configure how the model will call functions: + + - `auto`: allows the model to automatically choose the tool to use, or not call + a tool + - `none`: disables tool calling; the model will instead generate a message + - `required`: requires the model to call one or more tools + + You can also use a JSON object to force the model to call a specific tool. For + example, `{"type": "function", "function": {"name": "get_current_weather"}}` + requires the model to call the `get_current_weather` function, regardless of the + prompt. tools: An array containing tool definitions for tools that the model can use to generate responses. The tool definitions use JSON schema. You can define your own functions or use one of the built-in `graph`, `llm`, `translation`, or `vision` tools. Note that you can only use one built-in tool type in the array (only one of `graph`, `llm`, `translation`, or `vision`). You can pass multiple - [custom tools](https://dev.writer.com/api-guides/tool-calling) of type - `function` in the same request. + [custom tools](https://dev.writer.com/home/tool-calling) of type `function` in + the same request. top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with @@ -171,7 +182,7 @@ def chat( The response shown below is for non-streaming. To learn about streaming responses, see the - [chat completion guide](https://dev.writer.com/api-guides/chat-completion). + [chat completion guide](https://dev.writer.com/home/chat-completion). Args: messages: An array of message objects that form the conversation history or context for @@ -188,8 +199,10 @@ def chat( logprobs: Specifies whether to return log probabilities of the output tokens. max_tokens: Defines the maximum number of tokens (words and characters) that the model can - generate in the response. The default value is set to 16, but it can be adjusted - to allow for longer or shorter responses as needed. + generate in the response. This can be adjusted to allow for longer or shorter + responses as needed. The maximum value varies by model. See the + [models overview](/home/models) for more information about the maximum number of + tokens for each model. n: Specifies the number of completions (responses) to generate from the model in a single request. This parameter allows for generating multiple responses, @@ -212,17 +225,26 @@ def chat( temperature results in more varied and less predictable text, while a lower temperature produces more deterministic and conservative outputs. - tool_choice: Configure how the model will call functions: `auto` will allow the model to - automatically choose the best tool, `none` disables tool calling. You can also - pass a specific previously defined function. + tool_choice: + Configure how the model will call functions: + + - `auto`: allows the model to automatically choose the tool to use, or not call + a tool + - `none`: disables tool calling; the model will instead generate a message + - `required`: requires the model to call one or more tools + + You can also use a JSON object to force the model to call a specific tool. For + example, `{"type": "function", "function": {"name": "get_current_weather"}}` + requires the model to call the `get_current_weather` function, regardless of the + prompt. tools: An array containing tool definitions for tools that the model can use to generate responses. The tool definitions use JSON schema. You can define your own functions or use one of the built-in `graph`, `llm`, `translation`, or `vision` tools. Note that you can only use one built-in tool type in the array (only one of `graph`, `llm`, `translation`, or `vision`). You can pass multiple - [custom tools](https://dev.writer.com/api-guides/tool-calling) of type - `function` in the same request. + [custom tools](https://dev.writer.com/home/tool-calling) of type `function` in + the same request. top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with @@ -267,7 +289,7 @@ def chat( The response shown below is for non-streaming. To learn about streaming responses, see the - [chat completion guide](https://dev.writer.com/api-guides/chat-completion). + [chat completion guide](https://dev.writer.com/home/chat-completion). Args: messages: An array of message objects that form the conversation history or context for @@ -284,8 +306,10 @@ def chat( logprobs: Specifies whether to return log probabilities of the output tokens. max_tokens: Defines the maximum number of tokens (words and characters) that the model can - generate in the response. The default value is set to 16, but it can be adjusted - to allow for longer or shorter responses as needed. + generate in the response. This can be adjusted to allow for longer or shorter + responses as needed. The maximum value varies by model. See the + [models overview](/home/models) for more information about the maximum number of + tokens for each model. n: Specifies the number of completions (responses) to generate from the model in a single request. This parameter allows for generating multiple responses, @@ -308,17 +332,26 @@ def chat( temperature results in more varied and less predictable text, while a lower temperature produces more deterministic and conservative outputs. - tool_choice: Configure how the model will call functions: `auto` will allow the model to - automatically choose the best tool, `none` disables tool calling. You can also - pass a specific previously defined function. + tool_choice: + Configure how the model will call functions: + + - `auto`: allows the model to automatically choose the tool to use, or not call + a tool + - `none`: disables tool calling; the model will instead generate a message + - `required`: requires the model to call one or more tools + + You can also use a JSON object to force the model to call a specific tool. For + example, `{"type": "function", "function": {"name": "get_current_weather"}}` + requires the model to call the `get_current_weather` function, regardless of the + prompt. tools: An array containing tool definitions for tools that the model can use to generate responses. The tool definitions use JSON schema. You can define your own functions or use one of the built-in `graph`, `llm`, `translation`, or `vision` tools. Note that you can only use one built-in tool type in the array (only one of `graph`, `llm`, `translation`, or `vision`). You can pass multiple - [custom tools](https://dev.writer.com/api-guides/tool-calling) of type - `function` in the same request. + [custom tools](https://dev.writer.com/home/tool-calling) of type `function` in + the same request. top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with @@ -436,7 +469,7 @@ async def chat( The response shown below is for non-streaming. To learn about streaming responses, see the - [chat completion guide](https://dev.writer.com/api-guides/chat-completion). + [chat completion guide](https://dev.writer.com/home/chat-completion). Args: messages: An array of message objects that form the conversation history or context for @@ -449,8 +482,10 @@ async def chat( logprobs: Specifies whether to return log probabilities of the output tokens. max_tokens: Defines the maximum number of tokens (words and characters) that the model can - generate in the response. The default value is set to 16, but it can be adjusted - to allow for longer or shorter responses as needed. + generate in the response. This can be adjusted to allow for longer or shorter + responses as needed. The maximum value varies by model. See the + [models overview](/home/models) for more information about the maximum number of + tokens for each model. n: Specifies the number of completions (responses) to generate from the model in a single request. This parameter allows for generating multiple responses, @@ -477,17 +512,26 @@ async def chat( temperature results in more varied and less predictable text, while a lower temperature produces more deterministic and conservative outputs. - tool_choice: Configure how the model will call functions: `auto` will allow the model to - automatically choose the best tool, `none` disables tool calling. You can also - pass a specific previously defined function. + tool_choice: + Configure how the model will call functions: + + - `auto`: allows the model to automatically choose the tool to use, or not call + a tool + - `none`: disables tool calling; the model will instead generate a message + - `required`: requires the model to call one or more tools + + You can also use a JSON object to force the model to call a specific tool. For + example, `{"type": "function", "function": {"name": "get_current_weather"}}` + requires the model to call the `get_current_weather` function, regardless of the + prompt. tools: An array containing tool definitions for tools that the model can use to generate responses. The tool definitions use JSON schema. You can define your own functions or use one of the built-in `graph`, `llm`, `translation`, or `vision` tools. Note that you can only use one built-in tool type in the array (only one of `graph`, `llm`, `translation`, or `vision`). You can pass multiple - [custom tools](https://dev.writer.com/api-guides/tool-calling) of type - `function` in the same request. + [custom tools](https://dev.writer.com/home/tool-calling) of type `function` in + the same request. top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with @@ -532,7 +576,7 @@ async def chat( The response shown below is for non-streaming. To learn about streaming responses, see the - [chat completion guide](https://dev.writer.com/api-guides/chat-completion). + [chat completion guide](https://dev.writer.com/home/chat-completion). Args: messages: An array of message objects that form the conversation history or context for @@ -549,8 +593,10 @@ async def chat( logprobs: Specifies whether to return log probabilities of the output tokens. max_tokens: Defines the maximum number of tokens (words and characters) that the model can - generate in the response. The default value is set to 16, but it can be adjusted - to allow for longer or shorter responses as needed. + generate in the response. This can be adjusted to allow for longer or shorter + responses as needed. The maximum value varies by model. See the + [models overview](/home/models) for more information about the maximum number of + tokens for each model. n: Specifies the number of completions (responses) to generate from the model in a single request. This parameter allows for generating multiple responses, @@ -573,17 +619,26 @@ async def chat( temperature results in more varied and less predictable text, while a lower temperature produces more deterministic and conservative outputs. - tool_choice: Configure how the model will call functions: `auto` will allow the model to - automatically choose the best tool, `none` disables tool calling. You can also - pass a specific previously defined function. + tool_choice: + Configure how the model will call functions: + + - `auto`: allows the model to automatically choose the tool to use, or not call + a tool + - `none`: disables tool calling; the model will instead generate a message + - `required`: requires the model to call one or more tools + + You can also use a JSON object to force the model to call a specific tool. For + example, `{"type": "function", "function": {"name": "get_current_weather"}}` + requires the model to call the `get_current_weather` function, regardless of the + prompt. tools: An array containing tool definitions for tools that the model can use to generate responses. The tool definitions use JSON schema. You can define your own functions or use one of the built-in `graph`, `llm`, `translation`, or `vision` tools. Note that you can only use one built-in tool type in the array (only one of `graph`, `llm`, `translation`, or `vision`). You can pass multiple - [custom tools](https://dev.writer.com/api-guides/tool-calling) of type - `function` in the same request. + [custom tools](https://dev.writer.com/home/tool-calling) of type `function` in + the same request. top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with @@ -628,7 +683,7 @@ async def chat( The response shown below is for non-streaming. To learn about streaming responses, see the - [chat completion guide](https://dev.writer.com/api-guides/chat-completion). + [chat completion guide](https://dev.writer.com/home/chat-completion). Args: messages: An array of message objects that form the conversation history or context for @@ -645,8 +700,10 @@ async def chat( logprobs: Specifies whether to return log probabilities of the output tokens. max_tokens: Defines the maximum number of tokens (words and characters) that the model can - generate in the response. The default value is set to 16, but it can be adjusted - to allow for longer or shorter responses as needed. + generate in the response. This can be adjusted to allow for longer or shorter + responses as needed. The maximum value varies by model. See the + [models overview](/home/models) for more information about the maximum number of + tokens for each model. n: Specifies the number of completions (responses) to generate from the model in a single request. This parameter allows for generating multiple responses, @@ -669,17 +726,26 @@ async def chat( temperature results in more varied and less predictable text, while a lower temperature produces more deterministic and conservative outputs. - tool_choice: Configure how the model will call functions: `auto` will allow the model to - automatically choose the best tool, `none` disables tool calling. You can also - pass a specific previously defined function. + tool_choice: + Configure how the model will call functions: + + - `auto`: allows the model to automatically choose the tool to use, or not call + a tool + - `none`: disables tool calling; the model will instead generate a message + - `required`: requires the model to call one or more tools + + You can also use a JSON object to force the model to call a specific tool. For + example, `{"type": "function", "function": {"name": "get_current_weather"}}` + requires the model to call the `get_current_weather` function, regardless of the + prompt. tools: An array containing tool definitions for tools that the model can use to generate responses. The tool definitions use JSON schema. You can define your own functions or use one of the built-in `graph`, `llm`, `translation`, or `vision` tools. Note that you can only use one built-in tool type in the array (only one of `graph`, `llm`, `translation`, or `vision`). You can pass multiple - [custom tools](https://dev.writer.com/api-guides/tool-calling) of type - `function` in the same request. + [custom tools](https://dev.writer.com/home/tool-calling) of type `function` in + the same request. top_p: Sets the threshold for "nucleus sampling," a technique to focus the model's token generation on the most likely subset of tokens. Only tokens with diff --git a/src/writerai/resources/graphs.py b/src/writerai/resources/graphs.py index e93b2a67..e4ace645 100644 --- a/src/writerai/resources/graphs.py +++ b/src/writerai/resources/graphs.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import List +from typing import List, Iterable from typing_extensions import Literal, overload import httpx @@ -27,12 +27,13 @@ from .._streaming import Stream, AsyncStream from ..pagination import SyncCursorPage, AsyncCursorPage from ..types.file import File -from ..types.graph import Graph from .._base_client import AsyncPaginator, make_request_options from ..types.question import Question +from ..types.graph_list_response import GraphListResponse from ..types.graph_create_response import GraphCreateResponse from ..types.graph_delete_response import GraphDeleteResponse from ..types.graph_update_response import GraphUpdateResponse +from ..types.graph_retrieve_response import GraphRetrieveResponse from ..types.question_response_chunk import QuestionResponseChunk from ..types.graph_remove_file_from_graph_response import GraphRemoveFileFromGraphResponse @@ -114,7 +115,7 @@ def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Graph: + ) -> GraphRetrieveResponse: """ Retrieve a Knowledge Graph. @@ -134,7 +135,7 @@ def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Graph, + cast_to=GraphRetrieveResponse, ) def update( @@ -143,6 +144,7 @@ def update( *, description: str | NotGiven = NOT_GIVEN, name: str | NotGiven = NOT_GIVEN, + urls: Iterable[graph_update_params.URL] | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -160,6 +162,10 @@ def update( name: The name of the Knowledge Graph (max 255 characters). Omitting this field leaves the name unchanged. + urls: An array of web connector URLs to update for this Knowledge Graph. You can only + connect URLs to Knowledge Graphs with the type `web`. To clear the list of URLs, + set this field to an empty array. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -176,6 +182,7 @@ def update( { "description": description, "name": name, + "urls": urls, }, graph_update_params.GraphUpdateParams, ), @@ -198,7 +205,7 @@ def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SyncCursorPage[Graph]: + ) -> SyncCursorPage[GraphListResponse]: """ Retrieve a list of Knowledge Graphs. @@ -225,7 +232,7 @@ def list( """ return self._get_api_list( "/v1/graphs", - page=SyncCursorPage[Graph], + page=SyncCursorPage[GraphListResponse], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -241,7 +248,7 @@ def list( graph_list_params.GraphListParams, ), ), - model=Graph, + model=GraphListResponse, ) def delete( @@ -579,7 +586,7 @@ async def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Graph: + ) -> GraphRetrieveResponse: """ Retrieve a Knowledge Graph. @@ -599,7 +606,7 @@ async def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Graph, + cast_to=GraphRetrieveResponse, ) async def update( @@ -608,6 +615,7 @@ async def update( *, description: str | NotGiven = NOT_GIVEN, name: str | NotGiven = NOT_GIVEN, + urls: Iterable[graph_update_params.URL] | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -625,6 +633,10 @@ async def update( name: The name of the Knowledge Graph (max 255 characters). Omitting this field leaves the name unchanged. + urls: An array of web connector URLs to update for this Knowledge Graph. You can only + connect URLs to Knowledge Graphs with the type `web`. To clear the list of URLs, + set this field to an empty array. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -641,6 +653,7 @@ async def update( { "description": description, "name": name, + "urls": urls, }, graph_update_params.GraphUpdateParams, ), @@ -663,7 +676,7 @@ def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AsyncPaginator[Graph, AsyncCursorPage[Graph]]: + ) -> AsyncPaginator[GraphListResponse, AsyncCursorPage[GraphListResponse]]: """ Retrieve a list of Knowledge Graphs. @@ -690,7 +703,7 @@ def list( """ return self._get_api_list( "/v1/graphs", - page=AsyncCursorPage[Graph], + page=AsyncCursorPage[GraphListResponse], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -706,7 +719,7 @@ def list( graph_list_params.GraphListParams, ), ), - model=Graph, + model=GraphListResponse, ) async def delete( diff --git a/src/writerai/resources/translation.py b/src/writerai/resources/translation.py index f342bb6c..235ce8d8 100644 --- a/src/writerai/resources/translation.py +++ b/src/writerai/resources/translation.py @@ -65,15 +65,15 @@ def translate( Args: formality: Whether to use formal or informal language in the translation. See the - [list of languages that support formality](https://dev.writer.com/api-guides/api-reference/translation-api/language-support#formality). + [list of languages that support formality](https://dev.writer.com/api-reference/translation-api/language-support#formality). If the language does not support formality, this parameter is ignored. length_control: Whether to control the length of the translated text. See the - [list of languages that support length control](https://dev.writer.com/api-guides/api-reference/translation-api/language-support#length-control). + [list of languages that support length control](https://dev.writer.com/api-reference/translation-api/language-support#length-control). If the language does not support length control, this parameter is ignored. mask_profanity: Whether to mask profane words in the translated text. See the - [list of languages that do not support profanity masking](https://dev.writer.com/api-guides/api-reference/translation-api/language-support#profanity-masking). + [list of languages that do not support profanity masking](https://dev.writer.com/api-reference/translation-api/language-support#profanity-masking). If the language does not support profanity masking, this parameter is ignored. model: The model to use for translation. @@ -84,7 +84,7 @@ def translate( variant, the code appends the two-digit [ISO-3166 country code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes). For example, Mexican Spanish is `es-MX`. See the - [list of supported languages and language codes](https://dev.writer.com/api-guides/api-reference/translation-api/language-support). + [list of supported languages and language codes](https://dev.writer.com/api-reference/translation-api/language-support). target_language_code: The [ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes) language code of the target language for the translation. For example, `en` for @@ -92,7 +92,7 @@ def translate( has a variant, the code appends the two-digit [ISO-3166 country code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes). For example, Mexican Spanish is `es-MX`. See the - [list of supported languages and language codes](https://dev.writer.com/api-guides/api-reference/translation-api/language-support). + [list of supported languages and language codes](https://dev.writer.com/api-reference/translation-api/language-support). text: The text to translate. Maximum of 100,000 words. @@ -167,15 +167,15 @@ async def translate( Args: formality: Whether to use formal or informal language in the translation. See the - [list of languages that support formality](https://dev.writer.com/api-guides/api-reference/translation-api/language-support#formality). + [list of languages that support formality](https://dev.writer.com/api-reference/translation-api/language-support#formality). If the language does not support formality, this parameter is ignored. length_control: Whether to control the length of the translated text. See the - [list of languages that support length control](https://dev.writer.com/api-guides/api-reference/translation-api/language-support#length-control). + [list of languages that support length control](https://dev.writer.com/api-reference/translation-api/language-support#length-control). If the language does not support length control, this parameter is ignored. mask_profanity: Whether to mask profane words in the translated text. See the - [list of languages that do not support profanity masking](https://dev.writer.com/api-guides/api-reference/translation-api/language-support#profanity-masking). + [list of languages that do not support profanity masking](https://dev.writer.com/api-reference/translation-api/language-support#profanity-masking). If the language does not support profanity masking, this parameter is ignored. model: The model to use for translation. @@ -186,7 +186,7 @@ async def translate( variant, the code appends the two-digit [ISO-3166 country code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes). For example, Mexican Spanish is `es-MX`. See the - [list of supported languages and language codes](https://dev.writer.com/api-guides/api-reference/translation-api/language-support). + [list of supported languages and language codes](https://dev.writer.com/api-reference/translation-api/language-support). target_language_code: The [ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes) language code of the target language for the translation. For example, `en` for @@ -194,7 +194,7 @@ async def translate( has a variant, the code appends the two-digit [ISO-3166 country code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes). For example, Mexican Spanish is `es-MX`. See the - [list of supported languages and language codes](https://dev.writer.com/api-guides/api-reference/translation-api/language-support). + [list of supported languages and language codes](https://dev.writer.com/api-reference/translation-api/language-support). text: The text to translate. Maximum of 100,000 words. diff --git a/src/writerai/types/__init__.py b/src/writerai/types/__init__.py index 16b8f6ef..a71b1557 100644 --- a/src/writerai/types/__init__.py +++ b/src/writerai/types/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations from .file import File as File -from .graph import Graph as Graph from .shared import ( Source as Source, Logprobs as Logprobs, @@ -31,6 +30,7 @@ from .file_upload_params import FileUploadParams as FileUploadParams from .file_retry_response import FileRetryResponse as FileRetryResponse from .graph_create_params import GraphCreateParams as GraphCreateParams +from .graph_list_response import GraphListResponse as GraphListResponse from .graph_update_params import GraphUpdateParams as GraphUpdateParams from .model_list_response import ModelListResponse as ModelListResponse from .file_delete_response import FileDeleteResponse as FileDeleteResponse @@ -47,6 +47,7 @@ from .chat_completion_choice import ChatCompletionChoice as ChatCompletionChoice from .application_list_params import ApplicationListParams as ApplicationListParams from .chat_completion_message import ChatCompletionMessage as ChatCompletionMessage +from .graph_retrieve_response import GraphRetrieveResponse as GraphRetrieveResponse from .question_response_chunk import QuestionResponseChunk as QuestionResponseChunk from .tool_ai_detect_response import ToolAIDetectResponse as ToolAIDetectResponse from .tool_parse_pdf_response import ToolParsePdfResponse as ToolParsePdfResponse diff --git a/src/writerai/types/application_generate_content_params.py b/src/writerai/types/application_generate_content_params.py index 56b36867..bfd7011c 100644 --- a/src/writerai/types/application_generate_content_params.py +++ b/src/writerai/types/application_generate_content_params.py @@ -31,9 +31,9 @@ class Input(TypedDict, total=False): If the input type is "File upload", you must pass the `file_id` of an uploaded file. You cannot pass a file object directly. See the - [file upload endpoint](https://dev.writer.com/api-guides/api-reference/file-api/upload-files) + [file upload endpoint](https://dev.writer.com/api-reference/file-api/upload-files) for instructions on uploading files or the - [list files endpoint](https://dev.writer.com/api-guides/api-reference/file-api/get-all-files) + [list files endpoint](https://dev.writer.com/api-reference/file-api/get-all-files) for how to see a list of uploaded files and their IDs. """ diff --git a/src/writerai/types/applications/job_create_params.py b/src/writerai/types/applications/job_create_params.py index f0a908cd..56c84c46 100644 --- a/src/writerai/types/applications/job_create_params.py +++ b/src/writerai/types/applications/job_create_params.py @@ -27,8 +27,8 @@ class Input(TypedDict, total=False): If the input type is "File upload", you must pass the `file_id` of an uploaded file. You cannot pass a file object directly. See the - [file upload endpoint](https://dev.writer.com/api-guides/api-reference/file-api/upload-files) + [file upload endpoint](https://dev.writer.com/api-reference/file-api/upload-files) for instructions on uploading files or the - [list files endpoint](https://dev.writer.com/api-guides/api-reference/file-api/get-all-files) + [list files endpoint](https://dev.writer.com/api-reference/file-api/get-all-files) for how to see a list of uploaded files and their IDs. """ diff --git a/src/writerai/types/chat_chat_params.py b/src/writerai/types/chat_chat_params.py index 66b9a544..e3a8c854 100644 --- a/src/writerai/types/chat_chat_params.py +++ b/src/writerai/types/chat_chat_params.py @@ -42,8 +42,10 @@ class ChatChatParamsBase(TypedDict, total=False): max_tokens: int """ Defines the maximum number of tokens (words and characters) that the model can - generate in the response. The default value is set to 16, but it can be adjusted - to allow for longer or shorter responses as needed. + generate in the response. This can be adjusted to allow for longer or shorter + responses as needed. The maximum value varies by model. See the + [models overview](/home/models) for more information about the maximum number of + tokens for each model. """ n: int @@ -81,10 +83,17 @@ class ChatChatParamsBase(TypedDict, total=False): """ tool_choice: ToolChoice - """ - Configure how the model will call functions: `auto` will allow the model to - automatically choose the best tool, `none` disables tool calling. You can also - pass a specific previously defined function. + """Configure how the model will call functions: + + - `auto`: allows the model to automatically choose the tool to use, or not call + a tool + - `none`: disables tool calling; the model will instead generate a message + - `required`: requires the model to call one or more tools + + You can also use a JSON object to force the model to call a specific tool. For + example, `{"type": "function", "function": {"name": "get_current_weather"}}` + requires the model to call the `get_current_weather` function, regardless of the + prompt. """ tools: Iterable[ToolParam] @@ -94,8 +103,8 @@ class ChatChatParamsBase(TypedDict, total=False): own functions or use one of the built-in `graph`, `llm`, `translation`, or `vision` tools. Note that you can only use one built-in tool type in the array (only one of `graph`, `llm`, `translation`, or `vision`). You can pass multiple - [custom tools](https://dev.writer.com/api-guides/tool-calling) of type - `function` in the same request. + [custom tools](https://dev.writer.com/home/tool-calling) of type `function` in + the same request. """ top_p: float @@ -113,8 +122,8 @@ class Message(TypedDict, total=False): You can provide a system prompt by setting the role to `system`, or specify that a message is the result of a - [tool call](https://dev.writer.com/api-guides/tool-calling) by setting the role - to `tool`. + [tool call](https://dev.writer.com/home/tool-calling) by setting the role to + `tool`. """ content: Optional[str] diff --git a/src/writerai/types/graph.py b/src/writerai/types/graph.py deleted file mode 100644 index 02dcf94e..00000000 --- a/src/writerai/types/graph.py +++ /dev/null @@ -1,45 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional -from datetime import datetime -from typing_extensions import Literal - -from .._models import BaseModel - -__all__ = ["Graph", "FileStatus"] - - -class FileStatus(BaseModel): - completed: int - """The number of files that have been successfully processed.""" - - failed: int - """The number of files that failed to process.""" - - in_progress: int - """The number of files currently being processed.""" - - total: int - """The total number of files associated with the Knowledge Graph.""" - - -class Graph(BaseModel): - id: str - """The unique identifier of the Knowledge Graph.""" - - created_at: datetime - """The timestamp when the Knowledge Graph was created.""" - - file_status: FileStatus - - name: str - """The name of the Knowledge Graph.""" - - type: Literal["manual", "connector"] - """ - The type of Knowledge Graph, either `manual` (files are uploaded via UI or API) - or `connector` (files are uploaded via a connector). - """ - - description: Optional[str] = None - """A description of the Knowledge Graph.""" diff --git a/src/writerai/types/graph_create_response.py b/src/writerai/types/graph_create_response.py index 7564f4f8..11dcb958 100644 --- a/src/writerai/types/graph_create_response.py +++ b/src/writerai/types/graph_create_response.py @@ -1,11 +1,36 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional +from typing import List, Optional from datetime import datetime +from typing_extensions import Literal from .._models import BaseModel -__all__ = ["GraphCreateResponse"] +__all__ = ["GraphCreateResponse", "URL", "URLStatus"] + + +class URLStatus(BaseModel): + status: Literal["validating", "success", "error"] + """The current status of the URL processing.""" + + error_type: Optional[ + Literal["invalid_url", "not_searchable", "not_found", "paywall_or_login_page", "unexpected_error"] + ] = None + """The type of error that occurred during processing, if any.""" + + +class URL(BaseModel): + status: URLStatus + """The current status of the URL processing.""" + + type: Literal["single_page", "sub_pages"] + """The type of web connector processing for this URL.""" + + url: str + """The URL to be processed by the web connector.""" + + exclude_urls: Optional[List[str]] = None + """An array of URLs to exclude from processing within this web connector.""" class GraphCreateResponse(BaseModel): @@ -20,3 +45,6 @@ class GraphCreateResponse(BaseModel): description: Optional[str] = None """A description of the Knowledge Graph (max 255 characters).""" + + urls: Optional[List[URL]] = None + """An array of web connector URLs associated with this Knowledge Graph.""" diff --git a/src/writerai/types/graph_list_response.py b/src/writerai/types/graph_list_response.py new file mode 100644 index 00000000..babb4060 --- /dev/null +++ b/src/writerai/types/graph_list_response.py @@ -0,0 +1,76 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from datetime import datetime +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["GraphListResponse", "FileStatus", "URL", "URLStatus"] + + +class FileStatus(BaseModel): + completed: int + """The number of files that have been successfully processed.""" + + failed: int + """The number of files that failed to process.""" + + in_progress: int + """The number of files currently being processed.""" + + total: int + """The total number of files associated with the Knowledge Graph.""" + + +class URLStatus(BaseModel): + status: Literal["validating", "success", "error"] + """The current status of the URL processing.""" + + error_type: Optional[ + Literal["invalid_url", "not_searchable", "not_found", "paywall_or_login_page", "unexpected_error"] + ] = None + """The type of error that occurred during processing, if any.""" + + +class URL(BaseModel): + status: URLStatus + """The current status of the URL processing.""" + + type: Literal["single_page", "sub_pages"] + """The type of web connector processing for this URL.""" + + url: str + """The URL to be processed by the web connector.""" + + exclude_urls: Optional[List[str]] = None + """An array of URLs to exclude from processing within this web connector.""" + + +class GraphListResponse(BaseModel): + id: str + """The unique identifier of the Knowledge Graph.""" + + created_at: datetime + """The timestamp when the Knowledge Graph was created.""" + + file_status: FileStatus + """The processing status of files in the Knowledge Graph.""" + + name: str + """The name of the Knowledge Graph.""" + + type: Literal["manual", "connector", "web"] + """The type of Knowledge Graph. + + - `manual`: files are uploaded via UI or API + - `connector`: files are uploaded via a data connector such as Google Drive or + Confluence + - `web`: URLs are connected to the Knowledge Graph + """ + + description: Optional[str] = None + """A description of the Knowledge Graph.""" + + urls: Optional[List[URL]] = None + """An array of web connector URLs associated with this Knowledge Graph.""" diff --git a/src/writerai/types/graph_retrieve_response.py b/src/writerai/types/graph_retrieve_response.py new file mode 100644 index 00000000..f93f7149 --- /dev/null +++ b/src/writerai/types/graph_retrieve_response.py @@ -0,0 +1,76 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from datetime import datetime +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["GraphRetrieveResponse", "FileStatus", "URL", "URLStatus"] + + +class FileStatus(BaseModel): + completed: int + """The number of files that have been successfully processed.""" + + failed: int + """The number of files that failed to process.""" + + in_progress: int + """The number of files currently being processed.""" + + total: int + """The total number of files associated with the Knowledge Graph.""" + + +class URLStatus(BaseModel): + status: Literal["validating", "success", "error"] + """The current status of the URL processing.""" + + error_type: Optional[ + Literal["invalid_url", "not_searchable", "not_found", "paywall_or_login_page", "unexpected_error"] + ] = None + """The type of error that occurred during processing, if any.""" + + +class URL(BaseModel): + status: URLStatus + """The current status of the URL processing.""" + + type: Literal["single_page", "sub_pages"] + """The type of web connector processing for this URL.""" + + url: str + """The URL to be processed by the web connector.""" + + exclude_urls: Optional[List[str]] = None + """An array of URLs to exclude from processing within this web connector.""" + + +class GraphRetrieveResponse(BaseModel): + id: str + """The unique identifier of the Knowledge Graph.""" + + created_at: datetime + """The timestamp when the Knowledge Graph was created.""" + + file_status: FileStatus + """The processing status of files in the Knowledge Graph.""" + + name: str + """The name of the Knowledge Graph.""" + + type: Literal["manual", "connector", "web"] + """The type of Knowledge Graph. + + - `manual`: files are uploaded via UI or API + - `connector`: files are uploaded via a data connector such as Google Drive or + Confluence + - `web`: URLs are connected to the Knowledge Graph + """ + + description: Optional[str] = None + """A description of the Knowledge Graph.""" + + urls: Optional[List[URL]] = None + """An array of web connector URLs associated with this Knowledge Graph.""" diff --git a/src/writerai/types/graph_update_params.py b/src/writerai/types/graph_update_params.py index 03d36d52..7be9324d 100644 --- a/src/writerai/types/graph_update_params.py +++ b/src/writerai/types/graph_update_params.py @@ -2,9 +2,10 @@ from __future__ import annotations -from typing_extensions import TypedDict +from typing import List, Iterable +from typing_extensions import Literal, Required, TypedDict -__all__ = ["GraphUpdateParams"] +__all__ = ["GraphUpdateParams", "URL"] class GraphUpdateParams(TypedDict, total=False): @@ -19,3 +20,21 @@ class GraphUpdateParams(TypedDict, total=False): Omitting this field leaves the name unchanged. """ + + urls: Iterable[URL] + """An array of web connector URLs to update for this Knowledge Graph. + + You can only connect URLs to Knowledge Graphs with the type `web`. To clear the + list of URLs, set this field to an empty array. + """ + + +class URL(TypedDict, total=False): + type: Required[Literal["single_page", "sub_pages"]] + """The type of web connector processing for this URL.""" + + url: Required[str] + """The URL to be processed by the web connector.""" + + exclude_urls: List[str] + """An array of URLs to exclude from processing within this web connector.""" diff --git a/src/writerai/types/graph_update_response.py b/src/writerai/types/graph_update_response.py index 3e43b2e5..bc17ebf3 100644 --- a/src/writerai/types/graph_update_response.py +++ b/src/writerai/types/graph_update_response.py @@ -1,11 +1,36 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional +from typing import List, Optional from datetime import datetime +from typing_extensions import Literal from .._models import BaseModel -__all__ = ["GraphUpdateResponse"] +__all__ = ["GraphUpdateResponse", "URL", "URLStatus"] + + +class URLStatus(BaseModel): + status: Literal["validating", "success", "error"] + """The current status of the URL processing.""" + + error_type: Optional[ + Literal["invalid_url", "not_searchable", "not_found", "paywall_or_login_page", "unexpected_error"] + ] = None + """The type of error that occurred during processing, if any.""" + + +class URL(BaseModel): + status: URLStatus + """The current status of the URL processing.""" + + type: Literal["single_page", "sub_pages"] + """The type of web connector processing for this URL.""" + + url: str + """The URL to be processed by the web connector.""" + + exclude_urls: Optional[List[str]] = None + """An array of URLs to exclude from processing within this web connector.""" class GraphUpdateResponse(BaseModel): @@ -20,3 +45,6 @@ class GraphUpdateResponse(BaseModel): description: Optional[str] = None """A description of the Knowledge Graph (max 255 characters).""" + + urls: Optional[List[URL]] = None + """An array of web connector URLs associated with this Knowledge Graph.""" diff --git a/src/writerai/types/shared/tool_choice_json_object.py b/src/writerai/types/shared/tool_choice_json_object.py index bc12acf5..499b6d24 100644 --- a/src/writerai/types/shared/tool_choice_json_object.py +++ b/src/writerai/types/shared/tool_choice_json_object.py @@ -9,3 +9,7 @@ class ToolChoiceJsonObject(BaseModel): value: Dict[str, object] + """A JSON object that specifies the tool to call. + + For example, `{"type": "function", "function": {"name": "get_current_weather"}}` + """ diff --git a/src/writerai/types/shared/tool_param.py b/src/writerai/types/shared/tool_param.py index aa76ed45..4cf0039e 100644 --- a/src/writerai/types/shared/tool_param.py +++ b/src/writerai/types/shared/tool_param.py @@ -70,7 +70,7 @@ class TranslationToolFunction(BaseModel): """Whether to use formal or informal language in the translation. See the - [list of languages that support formality](https://dev.writer.com/api-guides/api-reference/translation-api/language-support#formality). + [list of languages that support formality](https://dev.writer.com/api-reference/translation-api/language-support#formality). If the language does not support formality, this parameter is ignored. """ @@ -78,7 +78,7 @@ class TranslationToolFunction(BaseModel): """Whether to control the length of the translated text. See the - [list of languages that support length control](https://dev.writer.com/api-guides/api-reference/translation-api/language-support#length-control). + [list of languages that support length control](https://dev.writer.com/api-reference/translation-api/language-support#length-control). If the language does not support length control, this parameter is ignored. """ @@ -86,7 +86,7 @@ class TranslationToolFunction(BaseModel): """Whether to mask profane words in the translated text. See the - [list of languages that do not support profanity masking](https://dev.writer.com/api-guides/api-reference/translation-api/language-support#profanity-masking). + [list of languages that do not support profanity masking](https://dev.writer.com/api-reference/translation-api/language-support#profanity-masking). If the language does not support profanity masking, this parameter is ignored. """ @@ -130,7 +130,7 @@ class VisionToolFunctionVariable(BaseModel): """The File ID of the image to analyze. The file must be uploaded to the Writer platform before you use it with the - Vision tool. + Vision tool. The maximum allowed file size is 7MB. """ name: str diff --git a/src/writerai/types/shared_params/tool_choice_json_object.py b/src/writerai/types/shared_params/tool_choice_json_object.py index 4b2cdbdc..30d0f7f6 100644 --- a/src/writerai/types/shared_params/tool_choice_json_object.py +++ b/src/writerai/types/shared_params/tool_choice_json_object.py @@ -10,3 +10,7 @@ class ToolChoiceJsonObject(TypedDict, total=False): value: Required[Dict[str, object]] + """A JSON object that specifies the tool to call. + + For example, `{"type": "function", "function": {"name": "get_current_weather"}}` + """ diff --git a/src/writerai/types/shared_params/tool_param.py b/src/writerai/types/shared_params/tool_param.py index c50863e4..3345372f 100644 --- a/src/writerai/types/shared_params/tool_param.py +++ b/src/writerai/types/shared_params/tool_param.py @@ -70,7 +70,7 @@ class TranslationToolFunction(TypedDict, total=False): """Whether to use formal or informal language in the translation. See the - [list of languages that support formality](https://dev.writer.com/api-guides/api-reference/translation-api/language-support#formality). + [list of languages that support formality](https://dev.writer.com/api-reference/translation-api/language-support#formality). If the language does not support formality, this parameter is ignored. """ @@ -78,7 +78,7 @@ class TranslationToolFunction(TypedDict, total=False): """Whether to control the length of the translated text. See the - [list of languages that support length control](https://dev.writer.com/api-guides/api-reference/translation-api/language-support#length-control). + [list of languages that support length control](https://dev.writer.com/api-reference/translation-api/language-support#length-control). If the language does not support length control, this parameter is ignored. """ @@ -86,7 +86,7 @@ class TranslationToolFunction(TypedDict, total=False): """Whether to mask profane words in the translated text. See the - [list of languages that do not support profanity masking](https://dev.writer.com/api-guides/api-reference/translation-api/language-support#profanity-masking). + [list of languages that do not support profanity masking](https://dev.writer.com/api-reference/translation-api/language-support#profanity-masking). If the language does not support profanity masking, this parameter is ignored. """ @@ -130,7 +130,7 @@ class VisionToolFunctionVariable(TypedDict, total=False): """The File ID of the image to analyze. The file must be uploaded to the Writer platform before you use it with the - Vision tool. + Vision tool. The maximum allowed file size is 7MB. """ name: Required[str] diff --git a/src/writerai/types/translation_translate_params.py b/src/writerai/types/translation_translate_params.py index 636618f1..26de74d7 100644 --- a/src/writerai/types/translation_translate_params.py +++ b/src/writerai/types/translation_translate_params.py @@ -12,7 +12,7 @@ class TranslationTranslateParams(TypedDict, total=False): """Whether to use formal or informal language in the translation. See the - [list of languages that support formality](https://dev.writer.com/api-guides/api-reference/translation-api/language-support#formality). + [list of languages that support formality](https://dev.writer.com/api-reference/translation-api/language-support#formality). If the language does not support formality, this parameter is ignored. """ @@ -20,7 +20,7 @@ class TranslationTranslateParams(TypedDict, total=False): """Whether to control the length of the translated text. See the - [list of languages that support length control](https://dev.writer.com/api-guides/api-reference/translation-api/language-support#length-control). + [list of languages that support length control](https://dev.writer.com/api-reference/translation-api/language-support#length-control). If the language does not support length control, this parameter is ignored. """ @@ -28,7 +28,7 @@ class TranslationTranslateParams(TypedDict, total=False): """Whether to mask profane words in the translated text. See the - [list of languages that do not support profanity masking](https://dev.writer.com/api-guides/api-reference/translation-api/language-support#profanity-masking). + [list of languages that do not support profanity masking](https://dev.writer.com/api-reference/translation-api/language-support#profanity-masking). If the language does not support profanity masking, this parameter is ignored. """ @@ -43,7 +43,7 @@ class TranslationTranslateParams(TypedDict, total=False): variant, the code appends the two-digit [ISO-3166 country code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes). For example, Mexican Spanish is `es-MX`. See the - [list of supported languages and language codes](https://dev.writer.com/api-guides/api-reference/translation-api/language-support). + [list of supported languages and language codes](https://dev.writer.com/api-reference/translation-api/language-support). """ target_language_code: Required[str] @@ -54,7 +54,7 @@ class TranslationTranslateParams(TypedDict, total=False): has a variant, the code appends the two-digit [ISO-3166 country code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes). For example, Mexican Spanish is `es-MX`. See the - [list of supported languages and language codes](https://dev.writer.com/api-guides/api-reference/translation-api/language-support). + [list of supported languages and language codes](https://dev.writer.com/api-reference/translation-api/language-support). """ text: Required[str] diff --git a/tests/api_resources/test_graphs.py b/tests/api_resources/test_graphs.py index 15ae7a62..102cbb9e 100644 --- a/tests/api_resources/test_graphs.py +++ b/tests/api_resources/test_graphs.py @@ -11,11 +11,12 @@ from tests.utils import assert_matches_type from writerai.types import ( File, - Graph, Question, + GraphListResponse, GraphCreateResponse, GraphDeleteResponse, GraphUpdateResponse, + GraphRetrieveResponse, GraphRemoveFileFromGraphResponse, ) from writerai.pagination import SyncCursorPage, AsyncCursorPage @@ -64,7 +65,7 @@ def test_method_retrieve(self, client: Writer) -> None: graph = client.graphs.retrieve( "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) - assert_matches_type(Graph, graph, path=["response"]) + assert_matches_type(GraphRetrieveResponse, graph, path=["response"]) @parametrize def test_raw_response_retrieve(self, client: Writer) -> None: @@ -75,7 +76,7 @@ def test_raw_response_retrieve(self, client: Writer) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" graph = response.parse() - assert_matches_type(Graph, graph, path=["response"]) + assert_matches_type(GraphRetrieveResponse, graph, path=["response"]) @parametrize def test_streaming_response_retrieve(self, client: Writer) -> None: @@ -86,7 +87,7 @@ def test_streaming_response_retrieve(self, client: Writer) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" graph = response.parse() - assert_matches_type(Graph, graph, path=["response"]) + assert_matches_type(GraphRetrieveResponse, graph, path=["response"]) assert cast(Any, response.is_closed) is True @@ -110,6 +111,13 @@ def test_method_update_with_all_params(self, client: Writer) -> None: graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", + urls=[ + { + "type": "single_page", + "url": "url", + "exclude_urls": ["string"], + } + ], ) assert_matches_type(GraphUpdateResponse, graph, path=["response"]) @@ -147,7 +155,7 @@ def test_path_params_update(self, client: Writer) -> None: @parametrize def test_method_list(self, client: Writer) -> None: graph = client.graphs.list() - assert_matches_type(SyncCursorPage[Graph], graph, path=["response"]) + assert_matches_type(SyncCursorPage[GraphListResponse], graph, path=["response"]) @parametrize def test_method_list_with_all_params(self, client: Writer) -> None: @@ -157,7 +165,7 @@ def test_method_list_with_all_params(self, client: Writer) -> None: limit=0, order="asc", ) - assert_matches_type(SyncCursorPage[Graph], graph, path=["response"]) + assert_matches_type(SyncCursorPage[GraphListResponse], graph, path=["response"]) @parametrize def test_raw_response_list(self, client: Writer) -> None: @@ -166,7 +174,7 @@ def test_raw_response_list(self, client: Writer) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" graph = response.parse() - assert_matches_type(SyncCursorPage[Graph], graph, path=["response"]) + assert_matches_type(SyncCursorPage[GraphListResponse], graph, path=["response"]) @parametrize def test_streaming_response_list(self, client: Writer) -> None: @@ -175,7 +183,7 @@ def test_streaming_response_list(self, client: Writer) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" graph = response.parse() - assert_matches_type(SyncCursorPage[Graph], graph, path=["response"]) + assert_matches_type(SyncCursorPage[GraphListResponse], graph, path=["response"]) assert cast(Any, response.is_closed) is True @@ -441,7 +449,7 @@ async def test_method_retrieve(self, async_client: AsyncWriter) -> None: graph = await async_client.graphs.retrieve( "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) - assert_matches_type(Graph, graph, path=["response"]) + assert_matches_type(GraphRetrieveResponse, graph, path=["response"]) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncWriter) -> None: @@ -452,7 +460,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncWriter) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" graph = await response.parse() - assert_matches_type(Graph, graph, path=["response"]) + assert_matches_type(GraphRetrieveResponse, graph, path=["response"]) @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncWriter) -> None: @@ -463,7 +471,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncWriter) -> N assert response.http_request.headers.get("X-Stainless-Lang") == "python" graph = await response.parse() - assert_matches_type(Graph, graph, path=["response"]) + assert_matches_type(GraphRetrieveResponse, graph, path=["response"]) assert cast(Any, response.is_closed) is True @@ -487,6 +495,13 @@ async def test_method_update_with_all_params(self, async_client: AsyncWriter) -> graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", + urls=[ + { + "type": "single_page", + "url": "url", + "exclude_urls": ["string"], + } + ], ) assert_matches_type(GraphUpdateResponse, graph, path=["response"]) @@ -524,7 +539,7 @@ async def test_path_params_update(self, async_client: AsyncWriter) -> None: @parametrize async def test_method_list(self, async_client: AsyncWriter) -> None: graph = await async_client.graphs.list() - assert_matches_type(AsyncCursorPage[Graph], graph, path=["response"]) + assert_matches_type(AsyncCursorPage[GraphListResponse], graph, path=["response"]) @parametrize async def test_method_list_with_all_params(self, async_client: AsyncWriter) -> None: @@ -534,7 +549,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncWriter) -> N limit=0, order="asc", ) - assert_matches_type(AsyncCursorPage[Graph], graph, path=["response"]) + assert_matches_type(AsyncCursorPage[GraphListResponse], graph, path=["response"]) @parametrize async def test_raw_response_list(self, async_client: AsyncWriter) -> None: @@ -543,7 +558,7 @@ async def test_raw_response_list(self, async_client: AsyncWriter) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" graph = await response.parse() - assert_matches_type(AsyncCursorPage[Graph], graph, path=["response"]) + assert_matches_type(AsyncCursorPage[GraphListResponse], graph, path=["response"]) @parametrize async def test_streaming_response_list(self, async_client: AsyncWriter) -> None: @@ -552,7 +567,7 @@ async def test_streaming_response_list(self, async_client: AsyncWriter) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" graph = await response.parse() - assert_matches_type(AsyncCursorPage[Graph], graph, path=["response"]) + assert_matches_type(AsyncCursorPage[GraphListResponse], graph, path=["response"]) assert cast(Any, response.is_closed) is True From ffcf538e4d42a4cd85e6c1bfa13789033f338b33 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 22 Jul 2025 12:05:23 +0000 Subject: [PATCH 286/399] fix(parsing): parse extra field types --- src/writerai/_models.py | 25 +++++++++++++++++++++++-- tests/test_models.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/writerai/_models.py b/src/writerai/_models.py index ffcbf67b..b8387ce9 100644 --- a/src/writerai/_models.py +++ b/src/writerai/_models.py @@ -208,14 +208,18 @@ def construct( # pyright: ignore[reportIncompatibleMethodOverride] else: fields_values[name] = field_get_default(field) + extra_field_type = _get_extra_fields_type(__cls) + _extra = {} for key, value in values.items(): if key not in model_fields: + parsed = construct_type(value=value, type_=extra_field_type) if extra_field_type is not None else value + if PYDANTIC_V2: - _extra[key] = value + _extra[key] = parsed else: _fields_set.add(key) - fields_values[key] = value + fields_values[key] = parsed object.__setattr__(m, "__dict__", fields_values) @@ -370,6 +374,23 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object: return construct_type(value=value, type_=type_, metadata=getattr(field, "metadata", None)) +def _get_extra_fields_type(cls: type[pydantic.BaseModel]) -> type | None: + if not PYDANTIC_V2: + # TODO + return None + + schema = cls.__pydantic_core_schema__ + if schema["type"] == "model": + fields = schema["schema"] + if fields["type"] == "model-fields": + extras = fields.get("extras_schema") + if extras and "cls" in extras: + # mypy can't narrow the type + return extras["cls"] # type: ignore[no-any-return] + + return None + + def is_basemodel(type_: type) -> bool: """Returns whether or not the given type is either a `BaseModel` or a union of `BaseModel`""" if is_union(type_): diff --git a/tests/test_models.py b/tests/test_models.py index 65019f23..0bf7e815 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,5 +1,5 @@ import json -from typing import Any, Dict, List, Union, Optional, cast +from typing import TYPE_CHECKING, Any, Dict, List, Union, Optional, cast from datetime import datetime, timezone from typing_extensions import Literal, Annotated, TypeAliasType @@ -934,3 +934,30 @@ class Type2(BaseModel): ) assert isinstance(model, Type1) assert isinstance(model.value, InnerType2) + + +@pytest.mark.skipif(not PYDANTIC_V2, reason="this is only supported in pydantic v2 for now") +def test_extra_properties() -> None: + class Item(BaseModel): + prop: int + + class Model(BaseModel): + __pydantic_extra__: Dict[str, Item] = Field(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + other: str + + if TYPE_CHECKING: + + def __getattr__(self, attr: str) -> Item: ... + + model = construct_type( + type_=Model, + value={ + "a": {"prop": 1}, + "other": "foo", + }, + ) + assert isinstance(model, Model) + assert model.a.prop == 1 + assert isinstance(model.a, Item) + assert model.other == "foo" From 044f420ce530f24dddf8bda67f2b494631f39419 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 24 Jul 2025 14:25:47 +0000 Subject: [PATCH 287/399] chore(project): add settings file for vscode --- .gitignore | 1 - .vscode/settings.json | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore index 87797408..95ceb189 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ .prism.log -.vscode _dev __pycache__ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..5b010307 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.analysis.importFormat": "relative", +} From bc3a3c8397a0472b416cf94c8d07f8fd0a1426d5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 30 Jul 2025 17:05:10 +0000 Subject: [PATCH 288/399] feat(client): support file upload requests --- src/writerai/_base_client.py | 5 ++++- src/writerai/_files.py | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index cc32903b..5fdf9390 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -532,7 +532,10 @@ def _build_request( is_body_allowed = options.method.lower() != "get" if is_body_allowed: - kwargs["json"] = json_data if is_given(json_data) else None + if isinstance(json_data, bytes): + kwargs["content"] = json_data + else: + kwargs["json"] = json_data if is_given(json_data) else None kwargs["files"] = files else: headers.pop("Content-Type", None) diff --git a/src/writerai/_files.py b/src/writerai/_files.py index 715cc207..cc14c14f 100644 --- a/src/writerai/_files.py +++ b/src/writerai/_files.py @@ -69,12 +69,12 @@ def _transform_file(file: FileTypes) -> HttpxFileTypes: return file if is_tuple_t(file): - return (file[0], _read_file_content(file[1]), *file[2:]) + return (file[0], read_file_content(file[1]), *file[2:]) raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple") -def _read_file_content(file: FileContent) -> HttpxFileContent: +def read_file_content(file: FileContent) -> HttpxFileContent: if isinstance(file, os.PathLike): return pathlib.Path(file).read_bytes() return file @@ -111,12 +111,12 @@ async def _async_transform_file(file: FileTypes) -> HttpxFileTypes: return file if is_tuple_t(file): - return (file[0], await _async_read_file_content(file[1]), *file[2:]) + return (file[0], await async_read_file_content(file[1]), *file[2:]) raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple") -async def _async_read_file_content(file: FileContent) -> HttpxFileContent: +async def async_read_file_content(file: FileContent) -> HttpxFileContent: if isinstance(file, os.PathLike): return await anyio.Path(file).read_bytes() From 04709052e58a31aee4b0949c26fca77ebcabda32 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 5 Aug 2025 11:25:18 +0000 Subject: [PATCH 289/399] chore(internal): fix ruff target version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 997f6913..4ecdef92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -159,7 +159,7 @@ reportPrivateUsage = false [tool.ruff] line-length = 120 output-format = "grouped" -target-version = "py37" +target-version = "py38" [tool.ruff.format] docstring-code-format = true From 43d6c08a37069df933caa284f6b2cdf78280527e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 17:58:33 +0000 Subject: [PATCH 290/399] chore: update @stainless-api/prism-cli to v5.15.0 --- scripts/mock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/mock b/scripts/mock index d2814ae6..0b28f6ea 100755 --- a/scripts/mock +++ b/scripts/mock @@ -21,7 +21,7 @@ echo "==> Starting mock server with URL ${URL}" # Run prism mock on the given spec if [ "$1" == "--daemon" ]; then - npm exec --package=@stainless-api/prism-cli@5.8.5 -- prism mock "$URL" &> .prism.log & + npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log & # Wait for server to come online echo -n "Waiting for server" @@ -37,5 +37,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stainless-api/prism-cli@5.8.5 -- prism mock "$URL" + npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" fi From 786508b208bad6669900f64dbed6927e6b90e836 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 21:24:00 +0000 Subject: [PATCH 291/399] chore(internal): update comment in script --- scripts/test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/test b/scripts/test index 2b878456..dbeda2d2 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! prism_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the prism command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stoplight/prism-cli@~5.3.2 -- prism mock path/to/your.openapi.yml${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock path/to/your.openapi.yml${NC}" echo exit 1 From 619c8ed139709fd2eb6721e7fb26f0abeee88078 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 20:01:38 +0000 Subject: [PATCH 292/399] docs(api): updates to API spec --- .stats.yml | 4 +- src/writerai/resources/completions.py | 36 ++++++---- src/writerai/resources/files.py | 68 ++++++++++++------- src/writerai/resources/models.py | 10 ++- src/writerai/types/chat_chat_params.py | 45 +++++++++++- src/writerai/types/chat_completion_message.py | 14 +++- src/writerai/types/shared/tool_param.py | 21 +++++- .../types/shared_params/tool_param.py | 20 +++++- tests/api_resources/test_chat.py | 8 +-- 9 files changed, 176 insertions(+), 50 deletions(-) diff --git a/.stats.yml b/.stats.yml index 40491d42..8c4e7c77 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 32 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-994b51f2c7c776c2cd3d4e3c6900cc6291da87296cea27921ec709a459a41034.yml -openapi_spec_hash: 1fdc6bb31a5464cebb4c579370764907 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-d4a152368a86404e58ce6fdf8966223006e5b4a15dbda4a3bb46ee331d57781d.yml +openapi_spec_hash: e951797c0506d6c69e0e2578d3dcf2db config_hash: c0c9f57ab19252f82cf765939edc61de diff --git a/src/writerai/resources/completions.py b/src/writerai/resources/completions.py index 4a1c971e..081ec326 100644 --- a/src/writerai/resources/completions.py +++ b/src/writerai/resources/completions.py @@ -66,8 +66,10 @@ def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> Completion: - """ - Text generation + """Generate text completions using the specified model and prompt. + + This endpoint is + useful for text generation tasks that don't require conversational context. Args: model: The [ID of the model](https://dev.writer.com/home/models) to use for generating @@ -129,8 +131,10 @@ def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> Stream[CompletionChunk]: - """ - Text generation + """Generate text completions using the specified model and prompt. + + This endpoint is + useful for text generation tasks that don't require conversational context. Args: model: The [ID of the model](https://dev.writer.com/home/models) to use for generating @@ -192,8 +196,10 @@ def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> Completion | Stream[CompletionChunk]: - """ - Text generation + """Generate text completions using the specified model and prompt. + + This endpoint is + useful for text generation tasks that don't require conversational context. Args: model: The [ID of the model](https://dev.writer.com/home/models) to use for generating @@ -322,8 +328,10 @@ async def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> Completion: - """ - Text generation + """Generate text completions using the specified model and prompt. + + This endpoint is + useful for text generation tasks that don't require conversational context. Args: model: The [ID of the model](https://dev.writer.com/home/models) to use for generating @@ -385,8 +393,10 @@ async def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> AsyncStream[CompletionChunk]: - """ - Text generation + """Generate text completions using the specified model and prompt. + + This endpoint is + useful for text generation tasks that don't require conversational context. Args: model: The [ID of the model](https://dev.writer.com/home/models) to use for generating @@ -448,8 +458,10 @@ async def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> Completion | AsyncStream[CompletionChunk]: - """ - Text generation + """Generate text completions using the specified model and prompt. + + This endpoint is + useful for text generation tasks that don't require conversational context. Args: model: The [ID of the model](https://dev.writer.com/home/models) to use for generating diff --git a/src/writerai/resources/files.py b/src/writerai/resources/files.py index cd93acc8..4e633bda 100644 --- a/src/writerai/resources/files.py +++ b/src/writerai/resources/files.py @@ -67,7 +67,8 @@ def retrieve( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> File: """ - Retrieve file + Retrieve detailed information about a specific file, including its metadata, + status, and associated graphs. Args: extra_headers: Send extra headers @@ -105,12 +106,12 @@ def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> SyncCursorPage[File]: - """List files + """ + Retrieve a paginated list of files with optional filtering by status, graph + association, and file type. Args: - after: The ID of the last object in the previous page. - - This parameter instructs the API + after: The ID of the last object in the previous page. This parameter instructs the API to return the next page of results. before: The ID of the first object in the previous page. This parameter instructs the @@ -173,8 +174,9 @@ def delete( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> FileDeleteResponse: - """ - Delete file + """Permanently delete a file from the system. + + This action cannot be undone. Args: extra_headers: Send extra headers @@ -206,8 +208,10 @@ def download( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> BinaryAPIResponse: - """ - Download file + """Download the binary content of a file. + + The response will contain the file data + in the appropriate MIME type. Args: extra_headers: Send extra headers @@ -240,8 +244,10 @@ def retry( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> FileRetryResponse: - """ - Retry failed files + """Retry processing of files that previously failed to process. + + This will + re-attempt the processing of the specified files. Args: file_ids: The unique identifier of the files to retry. @@ -275,8 +281,10 @@ def upload( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> File: - """ - Upload file + """Upload a new file to the system. + + Supports various file formats including PDF, + DOC, DOCX, PPT, PPTX, JPG, PNG, EML, HTML, SRT, CSV, XLS, and XLSX. Args: extra_headers: Send extra headers @@ -330,7 +338,8 @@ async def retrieve( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> File: """ - Retrieve file + Retrieve detailed information about a specific file, including its metadata, + status, and associated graphs. Args: extra_headers: Send extra headers @@ -368,12 +377,12 @@ def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> AsyncPaginator[File, AsyncCursorPage[File]]: - """List files + """ + Retrieve a paginated list of files with optional filtering by status, graph + association, and file type. Args: - after: The ID of the last object in the previous page. - - This parameter instructs the API + after: The ID of the last object in the previous page. This parameter instructs the API to return the next page of results. before: The ID of the first object in the previous page. This parameter instructs the @@ -436,8 +445,9 @@ async def delete( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> FileDeleteResponse: - """ - Delete file + """Permanently delete a file from the system. + + This action cannot be undone. Args: extra_headers: Send extra headers @@ -469,8 +479,10 @@ async def download( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> AsyncBinaryAPIResponse: - """ - Download file + """Download the binary content of a file. + + The response will contain the file data + in the appropriate MIME type. Args: extra_headers: Send extra headers @@ -503,8 +515,10 @@ async def retry( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> FileRetryResponse: - """ - Retry failed files + """Retry processing of files that previously failed to process. + + This will + re-attempt the processing of the specified files. Args: file_ids: The unique identifier of the files to retry. @@ -538,8 +552,10 @@ async def upload( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> File: - """ - Upload file + """Upload a new file to the system. + + Supports various file formats including PDF, + DOC, DOCX, PPT, PPTX, JPG, PNG, EML, HTML, SRT, CSV, XLS, and XLSX. Args: extra_headers: Send extra headers diff --git a/src/writerai/resources/models.py b/src/writerai/resources/models.py index 6714725e..b7515656 100644 --- a/src/writerai/resources/models.py +++ b/src/writerai/resources/models.py @@ -49,7 +49,10 @@ def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ModelListResponse: - """List models""" + """ + Retrieve a list of available models that can be used for text generation, chat + completions, and other AI tasks. + """ return self._get( "/v1/models", options=make_request_options( @@ -89,7 +92,10 @@ async def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ModelListResponse: - """List models""" + """ + Retrieve a list of available models that can be used for text generation, chat + completions, and other AI tasks. + """ return await self._get( "/v1/models", options=make_request_options( diff --git a/src/writerai/types/chat_chat_params.py b/src/writerai/types/chat_chat_params.py index e3a8c854..6b24bd80 100644 --- a/src/writerai/types/chat_chat_params.py +++ b/src/writerai/types/chat_chat_params.py @@ -14,6 +14,10 @@ __all__ = [ "ChatChatParamsBase", "Message", + "MessageContentUnionMember1", + "MessageContentUnionMember1TextFragment", + "MessageContentUnionMember1ImageFragment", + "MessageContentUnionMember1ImageFragmentImageURL", "ResponseFormat", "StreamOptions", "ToolChoice", @@ -116,6 +120,35 @@ class ChatChatParamsBase(TypedDict, total=False): """ +class MessageContentUnionMember1TextFragment(TypedDict, total=False): + text: Required[str] + """The actual text content of the message fragment.""" + + type: Required[Literal["text"]] + """The type of content fragment. Must be `text` for text fragments.""" + + +class MessageContentUnionMember1ImageFragmentImageURL(TypedDict, total=False): + url: Required[str] + """The URL pointing to the image file. + + Supports common image formats like JPEG, PNG, GIF, etc. + """ + + +class MessageContentUnionMember1ImageFragment(TypedDict, total=False): + image_url: Required[MessageContentUnionMember1ImageFragmentImageURL] + """The image URL object containing the location of the image.""" + + type: Required[Literal["image_url"]] + """The type of content fragment. Must be `image_url` for image fragments.""" + + +MessageContentUnionMember1: TypeAlias = Union[ + MessageContentUnionMember1TextFragment, MessageContentUnionMember1ImageFragment +] + + class Message(TypedDict, total=False): role: Required[Literal["user", "assistant", "system", "tool"]] """The role of the chat message. @@ -126,11 +159,21 @@ class Message(TypedDict, total=False): `tool`. """ - content: Optional[str] + content: Union[str, Iterable[MessageContentUnionMember1], None] + """The content of the message. + + Can be either a string (for text-only messages) or an array of content fragments + (for mixed text and image messages). + """ graph_data: Optional[GraphData] name: Optional[str] + """An optional name for the message sender. + + Useful for identifying different users, personas, or tools in multi-participant + conversations. + """ refusal: Optional[str] diff --git a/src/writerai/types/chat_completion_message.py b/src/writerai/types/chat_completion_message.py index acf5649d..5b4cb30b 100644 --- a/src/writerai/types/chat_completion_message.py +++ b/src/writerai/types/chat_completion_message.py @@ -7,7 +7,7 @@ from .shared.tool_call import ToolCall from .shared.graph_data import GraphData -__all__ = ["ChatCompletionMessage", "LlmData", "TranslationData"] +__all__ = ["ChatCompletionMessage", "LlmData", "TranslationData", "WebSearchData", "WebSearchDataSource"] class LlmData(BaseModel): @@ -29,6 +29,16 @@ class TranslationData(BaseModel): """The language code of the target text.""" +class WebSearchDataSource(BaseModel): + raw_content: Optional[str] = None + + url: Optional[str] = None + + +class WebSearchData(BaseModel): + sources: List[WebSearchDataSource] + + class ChatCompletionMessage(BaseModel): content: str """The text content produced by the model. @@ -49,3 +59,5 @@ class ChatCompletionMessage(BaseModel): tool_calls: Optional[List[ToolCall]] = None translation_data: Optional[TranslationData] = None + + web_search_data: Optional[WebSearchData] = None diff --git a/src/writerai/types/shared/tool_param.py b/src/writerai/types/shared/tool_param.py index 4cf0039e..f5186ef1 100644 --- a/src/writerai/types/shared/tool_param.py +++ b/src/writerai/types/shared/tool_param.py @@ -19,6 +19,8 @@ "VisionTool", "VisionToolFunction", "VisionToolFunctionVariable", + "WebSearchTool", + "WebSearchToolFunction", ] @@ -158,6 +160,23 @@ class VisionTool(BaseModel): """The type of tool.""" +class WebSearchToolFunction(BaseModel): + exclude_domains: List[str] + """An array of domains to exclude from the search results.""" + + include_domains: List[str] + """An array of domains to include in the search results.""" + + +class WebSearchTool(BaseModel): + function: WebSearchToolFunction + """A tool that uses web search to find information.""" + + type: Literal["web_search"] + """The type of tool.""" + + ToolParam: TypeAlias = Annotated[ - Union[FunctionTool, GraphTool, LlmTool, TranslationTool, VisionTool], PropertyInfo(discriminator="type") + Union[FunctionTool, GraphTool, LlmTool, TranslationTool, VisionTool, WebSearchTool], + PropertyInfo(discriminator="type"), ] diff --git a/src/writerai/types/shared_params/tool_param.py b/src/writerai/types/shared_params/tool_param.py index 3345372f..25211144 100644 --- a/src/writerai/types/shared_params/tool_param.py +++ b/src/writerai/types/shared_params/tool_param.py @@ -19,6 +19,8 @@ "VisionTool", "VisionToolFunction", "VisionToolFunctionVariable", + "WebSearchTool", + "WebSearchToolFunction", ] @@ -158,4 +160,20 @@ class VisionTool(TypedDict, total=False): """The type of tool.""" -ToolParam: TypeAlias = Union[FunctionTool, GraphTool, LlmTool, TranslationTool, VisionTool] +class WebSearchToolFunction(TypedDict, total=False): + exclude_domains: Required[List[str]] + """An array of domains to exclude from the search results.""" + + include_domains: Required[List[str]] + """An array of domains to include in the search results.""" + + +class WebSearchTool(TypedDict, total=False): + function: Required[WebSearchToolFunction] + """A tool that uses web search to find information.""" + + type: Required[Literal["web_search"]] + """The type of tool.""" + + +ToolParam: TypeAlias = Union[FunctionTool, GraphTool, LlmTool, TranslationTool, VisionTool, WebSearchTool] diff --git a/tests/api_resources/test_chat.py b/tests/api_resources/test_chat.py index f47c2ade..8d072394 100644 --- a/tests/api_resources/test_chat.py +++ b/tests/api_resources/test_chat.py @@ -31,7 +31,7 @@ def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: messages=[ { "role": "user", - "content": "content", + "content": "string", "graph_data": { "sources": [ { @@ -137,7 +137,7 @@ def test_method_chat_with_all_params_overload_2(self, client: Writer) -> None: messages=[ { "role": "user", - "content": "content", + "content": "string", "graph_data": { "sources": [ { @@ -249,7 +249,7 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW messages=[ { "role": "user", - "content": "content", + "content": "string", "graph_data": { "sources": [ { @@ -355,7 +355,7 @@ async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncW messages=[ { "role": "user", - "content": "content", + "content": "string", "graph_data": { "sources": [ { From a50739df1bc833c546084b28fee939f0e3c97622 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 21:26:52 +0000 Subject: [PATCH 293/399] docs(api): updates to API spec --- .stats.yml | 4 ++-- src/writerai/types/chat_chat_params.py | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.stats.yml b/.stats.yml index 8c4e7c77..3092ef7f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 32 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-d4a152368a86404e58ce6fdf8966223006e5b4a15dbda4a3bb46ee331d57781d.yml -openapi_spec_hash: e951797c0506d6c69e0e2578d3dcf2db +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-b7b778ba219aa40ff765c61fb47d444ff194ec186fa263b495ab26a06760f7d9.yml +openapi_spec_hash: 1a7020a16bfcfe0037bd0078ed2edda6 config_hash: c0c9f57ab19252f82cf765939edc61de diff --git a/src/writerai/types/chat_chat_params.py b/src/writerai/types/chat_chat_params.py index 6b24bd80..e15fb942 100644 --- a/src/writerai/types/chat_chat_params.py +++ b/src/writerai/types/chat_chat_params.py @@ -14,10 +14,10 @@ __all__ = [ "ChatChatParamsBase", "Message", - "MessageContentUnionMember1", - "MessageContentUnionMember1TextFragment", - "MessageContentUnionMember1ImageFragment", - "MessageContentUnionMember1ImageFragmentImageURL", + "MessageContentMixedContent", + "MessageContentMixedContentTextFragment", + "MessageContentMixedContentImageFragment", + "MessageContentMixedContentImageFragmentImageURL", "ResponseFormat", "StreamOptions", "ToolChoice", @@ -120,7 +120,7 @@ class ChatChatParamsBase(TypedDict, total=False): """ -class MessageContentUnionMember1TextFragment(TypedDict, total=False): +class MessageContentMixedContentTextFragment(TypedDict, total=False): text: Required[str] """The actual text content of the message fragment.""" @@ -128,7 +128,7 @@ class MessageContentUnionMember1TextFragment(TypedDict, total=False): """The type of content fragment. Must be `text` for text fragments.""" -class MessageContentUnionMember1ImageFragmentImageURL(TypedDict, total=False): +class MessageContentMixedContentImageFragmentImageURL(TypedDict, total=False): url: Required[str] """The URL pointing to the image file. @@ -136,16 +136,16 @@ class MessageContentUnionMember1ImageFragmentImageURL(TypedDict, total=False): """ -class MessageContentUnionMember1ImageFragment(TypedDict, total=False): - image_url: Required[MessageContentUnionMember1ImageFragmentImageURL] +class MessageContentMixedContentImageFragment(TypedDict, total=False): + image_url: Required[MessageContentMixedContentImageFragmentImageURL] """The image URL object containing the location of the image.""" type: Required[Literal["image_url"]] """The type of content fragment. Must be `image_url` for image fragments.""" -MessageContentUnionMember1: TypeAlias = Union[ - MessageContentUnionMember1TextFragment, MessageContentUnionMember1ImageFragment +MessageContentMixedContent: TypeAlias = Union[ + MessageContentMixedContentTextFragment, MessageContentMixedContentImageFragment ] @@ -159,7 +159,7 @@ class Message(TypedDict, total=False): `tool`. """ - content: Union[str, Iterable[MessageContentUnionMember1], None] + content: Union[str, Iterable[MessageContentMixedContent], None] """The content of the message. Can be either a string (for text-only messages) or an array of content fragments From 2777ebed940c9299473378e8e6ec4e4ee667ad79 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 21:34:53 +0000 Subject: [PATCH 294/399] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 3092ef7f..741a632c 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 32 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-b7b778ba219aa40ff765c61fb47d444ff194ec186fa263b495ab26a06760f7d9.yml -openapi_spec_hash: 1a7020a16bfcfe0037bd0078ed2edda6 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-eb5179f152e705f73cf3f2a75b7f3dfe364fd66649fed372619b351e7f3da72d.yml +openapi_spec_hash: 5deb6350478f0ba1d0df3d7aab441a5d config_hash: c0c9f57ab19252f82cf765939edc61de From 50568bcfe6a77a6d4714a09663c557ea8075a76a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 21:58:21 +0000 Subject: [PATCH 295/399] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 741a632c..a1eec2f7 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 32 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-eb5179f152e705f73cf3f2a75b7f3dfe364fd66649fed372619b351e7f3da72d.yml -openapi_spec_hash: 5deb6350478f0ba1d0df3d7aab441a5d +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-0bbaae2a69204cb3ccf6dc4fbb0cef810a4e8c78f09ac639e253e84df0add53a.yml +openapi_spec_hash: 0a9be554ca3af860e3831bd776e50f56 config_hash: c0c9f57ab19252f82cf765939edc61de From bb43d469b072e717d7fa95e3b27ec1de1d9dd95c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 22:04:22 +0000 Subject: [PATCH 296/399] feat(api): add web KG and web search --- .stats.yml | 4 +- api.md | 2 + src/writerai/resources/tools/tools.py | 567 +++++++++++++++++- src/writerai/types/__init__.py | 2 + src/writerai/types/tool_web_search_params.py | 245 ++++++++ .../types/tool_web_search_response.py | 32 + tests/api_resources/test_tools.py | 89 +++ 7 files changed, 938 insertions(+), 3 deletions(-) create mode 100644 src/writerai/types/tool_web_search_params.py create mode 100644 src/writerai/types/tool_web_search_response.py diff --git a/.stats.yml b/.stats.yml index a1eec2f7..9cd26179 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 32 +configured_endpoints: 33 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-0bbaae2a69204cb3ccf6dc4fbb0cef810a4e8c78f09ac639e253e84df0add53a.yml openapi_spec_hash: 0a9be554ca3af860e3831bd776e50f56 -config_hash: c0c9f57ab19252f82cf765939edc61de +config_hash: 7a38bab086b53b43d2a719cb4d883264 diff --git a/api.md b/api.md index aca1096e..d2641c43 100644 --- a/api.md +++ b/api.md @@ -168,6 +168,7 @@ from writerai.types import ( ToolAIDetectResponse, ToolContextAwareSplittingResponse, ToolParsePdfResponse, + ToolWebSearchResponse, ) ``` @@ -176,6 +177,7 @@ Methods: - client.tools.ai_detect(\*\*params) -> ToolAIDetectResponse - client.tools.context_aware_splitting(\*\*params) -> ToolContextAwareSplittingResponse - client.tools.parse_pdf(file_id, \*\*params) -> ToolParsePdfResponse +- client.tools.web_search(\*\*params) -> ToolWebSearchResponse ## Comprehend diff --git a/src/writerai/resources/tools/tools.py b/src/writerai/resources/tools/tools.py index 132ea276..cab8fb75 100644 --- a/src/writerai/resources/tools/tools.py +++ b/src/writerai/resources/tools/tools.py @@ -2,11 +2,17 @@ from __future__ import annotations +from typing import List, Union from typing_extensions import Literal import httpx -from ...types import tool_ai_detect_params, tool_parse_pdf_params, tool_context_aware_splitting_params +from ...types import ( + tool_ai_detect_params, + tool_parse_pdf_params, + tool_web_search_params, + tool_context_aware_splitting_params, +) from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven from ..._utils import maybe_transform, async_maybe_transform from ..._compat import cached_property @@ -28,6 +34,7 @@ from ..._base_client import make_request_options from ...types.tool_ai_detect_response import ToolAIDetectResponse from ...types.tool_parse_pdf_response import ToolParsePdfResponse +from ...types.tool_web_search_response import ToolWebSearchResponse from ...types.tool_context_aware_splitting_response import ToolContextAwareSplittingResponse __all__ = ["ToolsResource", "AsyncToolsResource"] @@ -177,6 +184,279 @@ def parse_pdf( cast_to=ToolParsePdfResponse, ) + def web_search( + self, + *, + chunks_per_source: int | NotGiven = NOT_GIVEN, + country: Literal[ + "afghanistan", + "albania", + "algeria", + "andorra", + "angola", + "argentina", + "armenia", + "australia", + "austria", + "azerbaijan", + "bahamas", + "bahrain", + "bangladesh", + "barbados", + "belarus", + "belgium", + "belize", + "benin", + "bhutan", + "bolivia", + "bosnia and herzegovina", + "botswana", + "brazil", + "brunei", + "bulgaria", + "burkina faso", + "burundi", + "cambodia", + "cameroon", + "canada", + "cape verde", + "central african republic", + "chad", + "chile", + "china", + "colombia", + "comoros", + "congo", + "costa rica", + "croatia", + "cuba", + "cyprus", + "czech republic", + "denmark", + "djibouti", + "dominican republic", + "ecuador", + "egypt", + "el salvador", + "equatorial guinea", + "eritrea", + "estonia", + "ethiopia", + "fiji", + "finland", + "france", + "gabon", + "gambia", + "georgia", + "germany", + "ghana", + "greece", + "guatemala", + "guinea", + "haiti", + "honduras", + "hungary", + "iceland", + "india", + "indonesia", + "iran", + "iraq", + "ireland", + "israel", + "italy", + "jamaica", + "japan", + "jordan", + "kazakhstan", + "kenya", + "kuwait", + "kyrgyzstan", + "latvia", + "lebanon", + "lesotho", + "liberia", + "libya", + "liechtenstein", + "lithuania", + "luxembourg", + "madagascar", + "malawi", + "malaysia", + "maldives", + "mali", + "malta", + "mauritania", + "mauritius", + "mexico", + "moldova", + "monaco", + "mongolia", + "montenegro", + "morocco", + "mozambique", + "myanmar", + "namibia", + "nepal", + "netherlands", + "new zealand", + "nicaragua", + "niger", + "nigeria", + "north korea", + "north macedonia", + "norway", + "oman", + "pakistan", + "panama", + "papua new guinea", + "paraguay", + "peru", + "philippines", + "poland", + "portugal", + "qatar", + "romania", + "russia", + "rwanda", + "saudi arabia", + "senegal", + "serbia", + "singapore", + "slovakia", + "slovenia", + "somalia", + "south africa", + "south korea", + "south sudan", + "spain", + "sri lanka", + "sudan", + "sweden", + "switzerland", + "syria", + "taiwan", + "tajikistan", + "tanzania", + "thailand", + "togo", + "trinidad and tobago", + "tunisia", + "turkey", + "turkmenistan", + "uganda", + "ukraine", + "united arab emirates", + "united kingdom", + "united states", + "uruguay", + "uzbekistan", + "venezuela", + "vietnam", + "yemen", + "zambia", + "zimbabwe", + ] + | NotGiven = NOT_GIVEN, + days: int | NotGiven = NOT_GIVEN, + exclude_domains: List[str] | NotGiven = NOT_GIVEN, + include_answer: bool | NotGiven = NOT_GIVEN, + include_domains: List[str] | NotGiven = NOT_GIVEN, + include_raw_content: Union[Literal["text", "markdown"], bool] | NotGiven = NOT_GIVEN, + max_results: int | NotGiven = NOT_GIVEN, + query: str | NotGiven = NOT_GIVEN, + search_depth: Literal["basic", "advanced"] | NotGiven = NOT_GIVEN, + stream: bool | NotGiven = NOT_GIVEN, + time_range: Literal["day", "week", "month", "year", "d", "w", "m", "y"] | NotGiven = NOT_GIVEN, + topic: Literal["general", "news"] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ToolWebSearchResponse: + """ + Search the web for information about a given query and return relevant results + with source URLs. + + Args: + chunks_per_source: Only applies when `search_depth` is `advanced`. Specifies how many text segments + to extract from each source. Limited to 3 chunks maximum. + + country: Localizes search results to a specific country. Only applies to general topic + searches. + + days: For news topic searches, specifies how many days of news coverage to include. + + exclude_domains: Domains to exclude from the search. If unset, the search includes all domains. + + include_answer: Whether to include a generated answer to the query in the response. If `false`, + only search results are returned. + + include_domains: Domains to include in the search. If unset, the search includes all domains. + + include_raw_content: + Controls how raw content is included in search results: + + - `text`: Returns plain text without formatting markup + - `markdown`: Returns structured content with markdown formatting (headers, + links, bold text) + - `true`: Same as `markdown` + - `false`: Raw content is not included (default if unset) + + max_results: Limits the number of search results returned. Cannot exceed 20 sources. + + query: The search query. + + search_depth: + Controls search comprehensiveness: + + - `basic`: Returns fewer but highly relevant results + - `advanced`: Performs a deeper search with more results + + stream: Enables streaming of search results as they become available. + + time_range: Filters results to content published within the specified time range back from + the current date. For example, `week` or `w` returns results from the past 7 + days. + + topic: The search topic category. Use `news` for current events and news articles, or + `general` for broader web search. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/v1/tools/web-search", + body=maybe_transform( + { + "chunks_per_source": chunks_per_source, + "country": country, + "days": days, + "exclude_domains": exclude_domains, + "include_answer": include_answer, + "include_domains": include_domains, + "include_raw_content": include_raw_content, + "max_results": max_results, + "query": query, + "search_depth": search_depth, + "stream": stream, + "time_range": time_range, + "topic": topic, + }, + tool_web_search_params.ToolWebSearchParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ToolWebSearchResponse, + ) + class AsyncToolsResource(AsyncAPIResource): @cached_property @@ -322,6 +602,279 @@ async def parse_pdf( cast_to=ToolParsePdfResponse, ) + async def web_search( + self, + *, + chunks_per_source: int | NotGiven = NOT_GIVEN, + country: Literal[ + "afghanistan", + "albania", + "algeria", + "andorra", + "angola", + "argentina", + "armenia", + "australia", + "austria", + "azerbaijan", + "bahamas", + "bahrain", + "bangladesh", + "barbados", + "belarus", + "belgium", + "belize", + "benin", + "bhutan", + "bolivia", + "bosnia and herzegovina", + "botswana", + "brazil", + "brunei", + "bulgaria", + "burkina faso", + "burundi", + "cambodia", + "cameroon", + "canada", + "cape verde", + "central african republic", + "chad", + "chile", + "china", + "colombia", + "comoros", + "congo", + "costa rica", + "croatia", + "cuba", + "cyprus", + "czech republic", + "denmark", + "djibouti", + "dominican republic", + "ecuador", + "egypt", + "el salvador", + "equatorial guinea", + "eritrea", + "estonia", + "ethiopia", + "fiji", + "finland", + "france", + "gabon", + "gambia", + "georgia", + "germany", + "ghana", + "greece", + "guatemala", + "guinea", + "haiti", + "honduras", + "hungary", + "iceland", + "india", + "indonesia", + "iran", + "iraq", + "ireland", + "israel", + "italy", + "jamaica", + "japan", + "jordan", + "kazakhstan", + "kenya", + "kuwait", + "kyrgyzstan", + "latvia", + "lebanon", + "lesotho", + "liberia", + "libya", + "liechtenstein", + "lithuania", + "luxembourg", + "madagascar", + "malawi", + "malaysia", + "maldives", + "mali", + "malta", + "mauritania", + "mauritius", + "mexico", + "moldova", + "monaco", + "mongolia", + "montenegro", + "morocco", + "mozambique", + "myanmar", + "namibia", + "nepal", + "netherlands", + "new zealand", + "nicaragua", + "niger", + "nigeria", + "north korea", + "north macedonia", + "norway", + "oman", + "pakistan", + "panama", + "papua new guinea", + "paraguay", + "peru", + "philippines", + "poland", + "portugal", + "qatar", + "romania", + "russia", + "rwanda", + "saudi arabia", + "senegal", + "serbia", + "singapore", + "slovakia", + "slovenia", + "somalia", + "south africa", + "south korea", + "south sudan", + "spain", + "sri lanka", + "sudan", + "sweden", + "switzerland", + "syria", + "taiwan", + "tajikistan", + "tanzania", + "thailand", + "togo", + "trinidad and tobago", + "tunisia", + "turkey", + "turkmenistan", + "uganda", + "ukraine", + "united arab emirates", + "united kingdom", + "united states", + "uruguay", + "uzbekistan", + "venezuela", + "vietnam", + "yemen", + "zambia", + "zimbabwe", + ] + | NotGiven = NOT_GIVEN, + days: int | NotGiven = NOT_GIVEN, + exclude_domains: List[str] | NotGiven = NOT_GIVEN, + include_answer: bool | NotGiven = NOT_GIVEN, + include_domains: List[str] | NotGiven = NOT_GIVEN, + include_raw_content: Union[Literal["text", "markdown"], bool] | NotGiven = NOT_GIVEN, + max_results: int | NotGiven = NOT_GIVEN, + query: str | NotGiven = NOT_GIVEN, + search_depth: Literal["basic", "advanced"] | NotGiven = NOT_GIVEN, + stream: bool | NotGiven = NOT_GIVEN, + time_range: Literal["day", "week", "month", "year", "d", "w", "m", "y"] | NotGiven = NOT_GIVEN, + topic: Literal["general", "news"] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ToolWebSearchResponse: + """ + Search the web for information about a given query and return relevant results + with source URLs. + + Args: + chunks_per_source: Only applies when `search_depth` is `advanced`. Specifies how many text segments + to extract from each source. Limited to 3 chunks maximum. + + country: Localizes search results to a specific country. Only applies to general topic + searches. + + days: For news topic searches, specifies how many days of news coverage to include. + + exclude_domains: Domains to exclude from the search. If unset, the search includes all domains. + + include_answer: Whether to include a generated answer to the query in the response. If `false`, + only search results are returned. + + include_domains: Domains to include in the search. If unset, the search includes all domains. + + include_raw_content: + Controls how raw content is included in search results: + + - `text`: Returns plain text without formatting markup + - `markdown`: Returns structured content with markdown formatting (headers, + links, bold text) + - `true`: Same as `markdown` + - `false`: Raw content is not included (default if unset) + + max_results: Limits the number of search results returned. Cannot exceed 20 sources. + + query: The search query. + + search_depth: + Controls search comprehensiveness: + + - `basic`: Returns fewer but highly relevant results + - `advanced`: Performs a deeper search with more results + + stream: Enables streaming of search results as they become available. + + time_range: Filters results to content published within the specified time range back from + the current date. For example, `week` or `w` returns results from the past 7 + days. + + topic: The search topic category. Use `news` for current events and news articles, or + `general` for broader web search. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/v1/tools/web-search", + body=await async_maybe_transform( + { + "chunks_per_source": chunks_per_source, + "country": country, + "days": days, + "exclude_domains": exclude_domains, + "include_answer": include_answer, + "include_domains": include_domains, + "include_raw_content": include_raw_content, + "max_results": max_results, + "query": query, + "search_depth": search_depth, + "stream": stream, + "time_range": time_range, + "topic": topic, + }, + tool_web_search_params.ToolWebSearchParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ToolWebSearchResponse, + ) + class ToolsResourceWithRawResponse: def __init__(self, tools: ToolsResource) -> None: @@ -336,6 +889,9 @@ def __init__(self, tools: ToolsResource) -> None: self.parse_pdf = to_raw_response_wrapper( tools.parse_pdf, ) + self.web_search = to_raw_response_wrapper( + tools.web_search, + ) @cached_property def comprehend(self) -> ComprehendResourceWithRawResponse: @@ -355,6 +911,9 @@ def __init__(self, tools: AsyncToolsResource) -> None: self.parse_pdf = async_to_raw_response_wrapper( tools.parse_pdf, ) + self.web_search = async_to_raw_response_wrapper( + tools.web_search, + ) @cached_property def comprehend(self) -> AsyncComprehendResourceWithRawResponse: @@ -374,6 +933,9 @@ def __init__(self, tools: ToolsResource) -> None: self.parse_pdf = to_streamed_response_wrapper( tools.parse_pdf, ) + self.web_search = to_streamed_response_wrapper( + tools.web_search, + ) @cached_property def comprehend(self) -> ComprehendResourceWithStreamingResponse: @@ -393,6 +955,9 @@ def __init__(self, tools: AsyncToolsResource) -> None: self.parse_pdf = async_to_streamed_response_wrapper( tools.parse_pdf, ) + self.web_search = async_to_streamed_response_wrapper( + tools.web_search, + ) @cached_property def comprehend(self) -> AsyncComprehendResourceWithStreamingResponse: diff --git a/src/writerai/types/__init__.py b/src/writerai/types/__init__.py index a71b1557..84a63a36 100644 --- a/src/writerai/types/__init__.py +++ b/src/writerai/types/__init__.py @@ -45,6 +45,7 @@ from .tool_parse_pdf_params import ToolParsePdfParams as ToolParsePdfParams from .vision_analyze_params import VisionAnalyzeParams as VisionAnalyzeParams from .chat_completion_choice import ChatCompletionChoice as ChatCompletionChoice +from .tool_web_search_params import ToolWebSearchParams as ToolWebSearchParams from .application_list_params import ApplicationListParams as ApplicationListParams from .chat_completion_message import ChatCompletionMessage as ChatCompletionMessage from .graph_retrieve_response import GraphRetrieveResponse as GraphRetrieveResponse @@ -52,6 +53,7 @@ from .tool_ai_detect_response import ToolAIDetectResponse as ToolAIDetectResponse from .tool_parse_pdf_response import ToolParsePdfResponse as ToolParsePdfResponse from .completion_create_params import CompletionCreateParams as CompletionCreateParams +from .tool_web_search_response import ToolWebSearchResponse as ToolWebSearchResponse from .application_list_response import ApplicationListResponse as ApplicationListResponse from .translation_translate_params import TranslationTranslateParams as TranslationTranslateParams from .application_retrieve_response import ApplicationRetrieveResponse as ApplicationRetrieveResponse diff --git a/src/writerai/types/tool_web_search_params.py b/src/writerai/types/tool_web_search_params.py new file mode 100644 index 00000000..800dc033 --- /dev/null +++ b/src/writerai/types/tool_web_search_params.py @@ -0,0 +1,245 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Union +from typing_extensions import Literal, TypedDict + +__all__ = ["ToolWebSearchParams"] + + +class ToolWebSearchParams(TypedDict, total=False): + chunks_per_source: int + """Only applies when `search_depth` is `advanced`. + + Specifies how many text segments to extract from each source. Limited to 3 + chunks maximum. + """ + + country: Literal[ + "afghanistan", + "albania", + "algeria", + "andorra", + "angola", + "argentina", + "armenia", + "australia", + "austria", + "azerbaijan", + "bahamas", + "bahrain", + "bangladesh", + "barbados", + "belarus", + "belgium", + "belize", + "benin", + "bhutan", + "bolivia", + "bosnia and herzegovina", + "botswana", + "brazil", + "brunei", + "bulgaria", + "burkina faso", + "burundi", + "cambodia", + "cameroon", + "canada", + "cape verde", + "central african republic", + "chad", + "chile", + "china", + "colombia", + "comoros", + "congo", + "costa rica", + "croatia", + "cuba", + "cyprus", + "czech republic", + "denmark", + "djibouti", + "dominican republic", + "ecuador", + "egypt", + "el salvador", + "equatorial guinea", + "eritrea", + "estonia", + "ethiopia", + "fiji", + "finland", + "france", + "gabon", + "gambia", + "georgia", + "germany", + "ghana", + "greece", + "guatemala", + "guinea", + "haiti", + "honduras", + "hungary", + "iceland", + "india", + "indonesia", + "iran", + "iraq", + "ireland", + "israel", + "italy", + "jamaica", + "japan", + "jordan", + "kazakhstan", + "kenya", + "kuwait", + "kyrgyzstan", + "latvia", + "lebanon", + "lesotho", + "liberia", + "libya", + "liechtenstein", + "lithuania", + "luxembourg", + "madagascar", + "malawi", + "malaysia", + "maldives", + "mali", + "malta", + "mauritania", + "mauritius", + "mexico", + "moldova", + "monaco", + "mongolia", + "montenegro", + "morocco", + "mozambique", + "myanmar", + "namibia", + "nepal", + "netherlands", + "new zealand", + "nicaragua", + "niger", + "nigeria", + "north korea", + "north macedonia", + "norway", + "oman", + "pakistan", + "panama", + "papua new guinea", + "paraguay", + "peru", + "philippines", + "poland", + "portugal", + "qatar", + "romania", + "russia", + "rwanda", + "saudi arabia", + "senegal", + "serbia", + "singapore", + "slovakia", + "slovenia", + "somalia", + "south africa", + "south korea", + "south sudan", + "spain", + "sri lanka", + "sudan", + "sweden", + "switzerland", + "syria", + "taiwan", + "tajikistan", + "tanzania", + "thailand", + "togo", + "trinidad and tobago", + "tunisia", + "turkey", + "turkmenistan", + "uganda", + "ukraine", + "united arab emirates", + "united kingdom", + "united states", + "uruguay", + "uzbekistan", + "venezuela", + "vietnam", + "yemen", + "zambia", + "zimbabwe", + ] + """Localizes search results to a specific country. + + Only applies to general topic searches. + """ + + days: int + """For news topic searches, specifies how many days of news coverage to include.""" + + exclude_domains: List[str] + """Domains to exclude from the search. If unset, the search includes all domains.""" + + include_answer: bool + """Whether to include a generated answer to the query in the response. + + If `false`, only search results are returned. + """ + + include_domains: List[str] + """Domains to include in the search. If unset, the search includes all domains.""" + + include_raw_content: Union[Literal["text", "markdown"], bool] + """Controls how raw content is included in search results: + + - `text`: Returns plain text without formatting markup + - `markdown`: Returns structured content with markdown formatting (headers, + links, bold text) + - `true`: Same as `markdown` + - `false`: Raw content is not included (default if unset) + """ + + max_results: int + """Limits the number of search results returned. Cannot exceed 20 sources.""" + + query: str + """The search query.""" + + search_depth: Literal["basic", "advanced"] + """Controls search comprehensiveness: + + - `basic`: Returns fewer but highly relevant results + - `advanced`: Performs a deeper search with more results + """ + + stream: bool + """Enables streaming of search results as they become available.""" + + time_range: Literal["day", "week", "month", "year", "d", "w", "m", "y"] + """ + Filters results to content published within the specified time range back from + the current date. For example, `week` or `w` returns results from the past 7 + days. + """ + + topic: Literal["general", "news"] + """The search topic category. + + Use `news` for current events and news articles, or `general` for broader web + search. + """ diff --git a/src/writerai/types/tool_web_search_response.py b/src/writerai/types/tool_web_search_response.py new file mode 100644 index 00000000..2ed9ed71 --- /dev/null +++ b/src/writerai/types/tool_web_search_response.py @@ -0,0 +1,32 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .._models import BaseModel + +__all__ = ["ToolWebSearchResponse", "Source"] + + +class Source(BaseModel): + raw_content: Optional[str] = None + """Raw content from the source URL. + + Not included if `include_raw_content` is `false`. + """ + + url: Optional[str] = None + """URL of the search result.""" + + +class ToolWebSearchResponse(BaseModel): + query: str + """The search query that was submitted.""" + + sources: List[Source] + """The search results found.""" + + answer: Optional[str] = None + """Generated answer based on the search results. + + Not included if `include_answer` is `false`. + """ diff --git a/tests/api_resources/test_tools.py b/tests/api_resources/test_tools.py index bbc2a7db..8e2787aa 100644 --- a/tests/api_resources/test_tools.py +++ b/tests/api_resources/test_tools.py @@ -12,6 +12,7 @@ from writerai.types import ( ToolAIDetectResponse, ToolParsePdfResponse, + ToolWebSearchResponse, ToolContextAwareSplittingResponse, ) @@ -128,6 +129,50 @@ def test_path_params_parse_pdf(self, client: Writer) -> None: format="text", ) + @parametrize + def test_method_web_search(self, client: Writer) -> None: + tool = client.tools.web_search() + assert_matches_type(ToolWebSearchResponse, tool, path=["response"]) + + @parametrize + def test_method_web_search_with_all_params(self, client: Writer) -> None: + tool = client.tools.web_search( + chunks_per_source=0, + country="afghanistan", + days=0, + exclude_domains=["string"], + include_answer=True, + include_domains=["dev.writer.com"], + include_raw_content="text", + max_results=0, + query="How do I get an API key for the Writer API?", + search_depth="basic", + stream=True, + time_range="day", + topic="general", + ) + assert_matches_type(ToolWebSearchResponse, tool, path=["response"]) + + @parametrize + def test_raw_response_web_search(self, client: Writer) -> None: + response = client.tools.with_raw_response.web_search() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + tool = response.parse() + assert_matches_type(ToolWebSearchResponse, tool, path=["response"]) + + @parametrize + def test_streaming_response_web_search(self, client: Writer) -> None: + with client.tools.with_streaming_response.web_search() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + tool = response.parse() + assert_matches_type(ToolWebSearchResponse, tool, path=["response"]) + + assert cast(Any, response.is_closed) is True + class TestAsyncTools: parametrize = pytest.mark.parametrize( @@ -240,3 +285,47 @@ async def test_path_params_parse_pdf(self, async_client: AsyncWriter) -> None: file_id="", format="text", ) + + @parametrize + async def test_method_web_search(self, async_client: AsyncWriter) -> None: + tool = await async_client.tools.web_search() + assert_matches_type(ToolWebSearchResponse, tool, path=["response"]) + + @parametrize + async def test_method_web_search_with_all_params(self, async_client: AsyncWriter) -> None: + tool = await async_client.tools.web_search( + chunks_per_source=0, + country="afghanistan", + days=0, + exclude_domains=["string"], + include_answer=True, + include_domains=["dev.writer.com"], + include_raw_content="text", + max_results=0, + query="How do I get an API key for the Writer API?", + search_depth="basic", + stream=True, + time_range="day", + topic="general", + ) + assert_matches_type(ToolWebSearchResponse, tool, path=["response"]) + + @parametrize + async def test_raw_response_web_search(self, async_client: AsyncWriter) -> None: + response = await async_client.tools.with_raw_response.web_search() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + tool = await response.parse() + assert_matches_type(ToolWebSearchResponse, tool, path=["response"]) + + @parametrize + async def test_streaming_response_web_search(self, async_client: AsyncWriter) -> None: + async with async_client.tools.with_streaming_response.web_search() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + tool = await response.parse() + assert_matches_type(ToolWebSearchResponse, tool, path=["response"]) + + assert cast(Any, response.is_closed) is True From a6a1aa361b31b9706bc0b26aab68be4ac7860334 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 14 Aug 2025 17:31:49 +0000 Subject: [PATCH 297/399] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 89d8ff81..75ec52fc 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.2.1" + ".": "2.3.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 4ecdef92..50f87e6c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "2.2.1" +version = "2.3.0" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index 4fba2a61..db468bf9 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "2.2.1" # x-release-please-version +__version__ = "2.3.0" # x-release-please-version From 1c03b82d739c0c3899144dba11f13c94ec85b0e4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 19:18:41 +0000 Subject: [PATCH 298/399] docs(api): updates to API spec --- .stats.yml | 4 +- api.md | 6 +- src/writerai/resources/graphs.py | 23 +++--- src/writerai/types/__init__.py | 3 +- .../{graph_list_response.py => graph.py} | 4 +- src/writerai/types/graph_retrieve_response.py | 76 ------------------- tests/api_resources/test_graphs.py | 31 ++++---- 7 files changed, 33 insertions(+), 114 deletions(-) rename src/writerai/types/{graph_list_response.py => graph.py} (95%) delete mode 100644 src/writerai/types/graph_retrieve_response.py diff --git a/.stats.yml b/.stats.yml index 9cd26179..cf180159 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 33 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-0bbaae2a69204cb3ccf6dc4fbb0cef810a4e8c78f09ac639e253e84df0add53a.yml -openapi_spec_hash: 0a9be554ca3af860e3831bd776e50f56 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-9e04c6a51c55704029c529f2917d0e2b976cb7b6595128697db031ad7bd61a63.yml +openapi_spec_hash: e8a95522dd13ffe4633cccc34bbd651d config_hash: 7a38bab086b53b43d2a719cb4d883264 diff --git a/api.md b/api.md index d2641c43..4569c139 100644 --- a/api.md +++ b/api.md @@ -123,9 +123,7 @@ from writerai.types import ( Question, QuestionResponseChunk, GraphCreateResponse, - GraphRetrieveResponse, GraphUpdateResponse, - GraphListResponse, GraphDeleteResponse, GraphRemoveFileFromGraphResponse, ) @@ -134,9 +132,9 @@ from writerai.types import ( Methods: - client.graphs.create(\*\*params) -> GraphCreateResponse -- client.graphs.retrieve(graph_id) -> GraphRetrieveResponse +- client.graphs.retrieve(graph_id) -> Graph - client.graphs.update(graph_id, \*\*params) -> GraphUpdateResponse -- client.graphs.list(\*\*params) -> SyncCursorPage[GraphListResponse] +- client.graphs.list(\*\*params) -> SyncCursorPage[Graph] - client.graphs.delete(graph_id) -> GraphDeleteResponse - client.graphs.add_file_to_graph(graph_id, \*\*params) -> File - client.graphs.question(\*\*params) -> Question diff --git a/src/writerai/resources/graphs.py b/src/writerai/resources/graphs.py index e4ace645..a427bb93 100644 --- a/src/writerai/resources/graphs.py +++ b/src/writerai/resources/graphs.py @@ -27,13 +27,12 @@ from .._streaming import Stream, AsyncStream from ..pagination import SyncCursorPage, AsyncCursorPage from ..types.file import File +from ..types.graph import Graph from .._base_client import AsyncPaginator, make_request_options from ..types.question import Question -from ..types.graph_list_response import GraphListResponse from ..types.graph_create_response import GraphCreateResponse from ..types.graph_delete_response import GraphDeleteResponse from ..types.graph_update_response import GraphUpdateResponse -from ..types.graph_retrieve_response import GraphRetrieveResponse from ..types.question_response_chunk import QuestionResponseChunk from ..types.graph_remove_file_from_graph_response import GraphRemoveFileFromGraphResponse @@ -115,7 +114,7 @@ def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> GraphRetrieveResponse: + ) -> Graph: """ Retrieve a Knowledge Graph. @@ -135,7 +134,7 @@ def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=GraphRetrieveResponse, + cast_to=Graph, ) def update( @@ -205,7 +204,7 @@ def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SyncCursorPage[GraphListResponse]: + ) -> SyncCursorPage[Graph]: """ Retrieve a list of Knowledge Graphs. @@ -232,7 +231,7 @@ def list( """ return self._get_api_list( "/v1/graphs", - page=SyncCursorPage[GraphListResponse], + page=SyncCursorPage[Graph], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -248,7 +247,7 @@ def list( graph_list_params.GraphListParams, ), ), - model=GraphListResponse, + model=Graph, ) def delete( @@ -586,7 +585,7 @@ async def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> GraphRetrieveResponse: + ) -> Graph: """ Retrieve a Knowledge Graph. @@ -606,7 +605,7 @@ async def retrieve( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=GraphRetrieveResponse, + cast_to=Graph, ) async def update( @@ -676,7 +675,7 @@ def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AsyncPaginator[GraphListResponse, AsyncCursorPage[GraphListResponse]]: + ) -> AsyncPaginator[Graph, AsyncCursorPage[Graph]]: """ Retrieve a list of Knowledge Graphs. @@ -703,7 +702,7 @@ def list( """ return self._get_api_list( "/v1/graphs", - page=AsyncCursorPage[GraphListResponse], + page=AsyncCursorPage[Graph], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -719,7 +718,7 @@ def list( graph_list_params.GraphListParams, ), ), - model=GraphListResponse, + model=Graph, ) async def delete( diff --git a/src/writerai/types/__init__.py b/src/writerai/types/__init__.py index 84a63a36..3ded6249 100644 --- a/src/writerai/types/__init__.py +++ b/src/writerai/types/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations from .file import File as File +from .graph import Graph as Graph from .shared import ( Source as Source, Logprobs as Logprobs, @@ -30,7 +31,6 @@ from .file_upload_params import FileUploadParams as FileUploadParams from .file_retry_response import FileRetryResponse as FileRetryResponse from .graph_create_params import GraphCreateParams as GraphCreateParams -from .graph_list_response import GraphListResponse as GraphListResponse from .graph_update_params import GraphUpdateParams as GraphUpdateParams from .model_list_response import ModelListResponse as ModelListResponse from .file_delete_response import FileDeleteResponse as FileDeleteResponse @@ -48,7 +48,6 @@ from .tool_web_search_params import ToolWebSearchParams as ToolWebSearchParams from .application_list_params import ApplicationListParams as ApplicationListParams from .chat_completion_message import ChatCompletionMessage as ChatCompletionMessage -from .graph_retrieve_response import GraphRetrieveResponse as GraphRetrieveResponse from .question_response_chunk import QuestionResponseChunk as QuestionResponseChunk from .tool_ai_detect_response import ToolAIDetectResponse as ToolAIDetectResponse from .tool_parse_pdf_response import ToolParsePdfResponse as ToolParsePdfResponse diff --git a/src/writerai/types/graph_list_response.py b/src/writerai/types/graph.py similarity index 95% rename from src/writerai/types/graph_list_response.py rename to src/writerai/types/graph.py index babb4060..469c0da6 100644 --- a/src/writerai/types/graph_list_response.py +++ b/src/writerai/types/graph.py @@ -6,7 +6,7 @@ from .._models import BaseModel -__all__ = ["GraphListResponse", "FileStatus", "URL", "URLStatus"] +__all__ = ["Graph", "FileStatus", "URL", "URLStatus"] class FileStatus(BaseModel): @@ -47,7 +47,7 @@ class URL(BaseModel): """An array of URLs to exclude from processing within this web connector.""" -class GraphListResponse(BaseModel): +class Graph(BaseModel): id: str """The unique identifier of the Knowledge Graph.""" diff --git a/src/writerai/types/graph_retrieve_response.py b/src/writerai/types/graph_retrieve_response.py deleted file mode 100644 index f93f7149..00000000 --- a/src/writerai/types/graph_retrieve_response.py +++ /dev/null @@ -1,76 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List, Optional -from datetime import datetime -from typing_extensions import Literal - -from .._models import BaseModel - -__all__ = ["GraphRetrieveResponse", "FileStatus", "URL", "URLStatus"] - - -class FileStatus(BaseModel): - completed: int - """The number of files that have been successfully processed.""" - - failed: int - """The number of files that failed to process.""" - - in_progress: int - """The number of files currently being processed.""" - - total: int - """The total number of files associated with the Knowledge Graph.""" - - -class URLStatus(BaseModel): - status: Literal["validating", "success", "error"] - """The current status of the URL processing.""" - - error_type: Optional[ - Literal["invalid_url", "not_searchable", "not_found", "paywall_or_login_page", "unexpected_error"] - ] = None - """The type of error that occurred during processing, if any.""" - - -class URL(BaseModel): - status: URLStatus - """The current status of the URL processing.""" - - type: Literal["single_page", "sub_pages"] - """The type of web connector processing for this URL.""" - - url: str - """The URL to be processed by the web connector.""" - - exclude_urls: Optional[List[str]] = None - """An array of URLs to exclude from processing within this web connector.""" - - -class GraphRetrieveResponse(BaseModel): - id: str - """The unique identifier of the Knowledge Graph.""" - - created_at: datetime - """The timestamp when the Knowledge Graph was created.""" - - file_status: FileStatus - """The processing status of files in the Knowledge Graph.""" - - name: str - """The name of the Knowledge Graph.""" - - type: Literal["manual", "connector", "web"] - """The type of Knowledge Graph. - - - `manual`: files are uploaded via UI or API - - `connector`: files are uploaded via a data connector such as Google Drive or - Confluence - - `web`: URLs are connected to the Knowledge Graph - """ - - description: Optional[str] = None - """A description of the Knowledge Graph.""" - - urls: Optional[List[URL]] = None - """An array of web connector URLs associated with this Knowledge Graph.""" diff --git a/tests/api_resources/test_graphs.py b/tests/api_resources/test_graphs.py index 102cbb9e..005c9cab 100644 --- a/tests/api_resources/test_graphs.py +++ b/tests/api_resources/test_graphs.py @@ -11,12 +11,11 @@ from tests.utils import assert_matches_type from writerai.types import ( File, + Graph, Question, - GraphListResponse, GraphCreateResponse, GraphDeleteResponse, GraphUpdateResponse, - GraphRetrieveResponse, GraphRemoveFileFromGraphResponse, ) from writerai.pagination import SyncCursorPage, AsyncCursorPage @@ -65,7 +64,7 @@ def test_method_retrieve(self, client: Writer) -> None: graph = client.graphs.retrieve( "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) - assert_matches_type(GraphRetrieveResponse, graph, path=["response"]) + assert_matches_type(Graph, graph, path=["response"]) @parametrize def test_raw_response_retrieve(self, client: Writer) -> None: @@ -76,7 +75,7 @@ def test_raw_response_retrieve(self, client: Writer) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" graph = response.parse() - assert_matches_type(GraphRetrieveResponse, graph, path=["response"]) + assert_matches_type(Graph, graph, path=["response"]) @parametrize def test_streaming_response_retrieve(self, client: Writer) -> None: @@ -87,7 +86,7 @@ def test_streaming_response_retrieve(self, client: Writer) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" graph = response.parse() - assert_matches_type(GraphRetrieveResponse, graph, path=["response"]) + assert_matches_type(Graph, graph, path=["response"]) assert cast(Any, response.is_closed) is True @@ -155,7 +154,7 @@ def test_path_params_update(self, client: Writer) -> None: @parametrize def test_method_list(self, client: Writer) -> None: graph = client.graphs.list() - assert_matches_type(SyncCursorPage[GraphListResponse], graph, path=["response"]) + assert_matches_type(SyncCursorPage[Graph], graph, path=["response"]) @parametrize def test_method_list_with_all_params(self, client: Writer) -> None: @@ -165,7 +164,7 @@ def test_method_list_with_all_params(self, client: Writer) -> None: limit=0, order="asc", ) - assert_matches_type(SyncCursorPage[GraphListResponse], graph, path=["response"]) + assert_matches_type(SyncCursorPage[Graph], graph, path=["response"]) @parametrize def test_raw_response_list(self, client: Writer) -> None: @@ -174,7 +173,7 @@ def test_raw_response_list(self, client: Writer) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" graph = response.parse() - assert_matches_type(SyncCursorPage[GraphListResponse], graph, path=["response"]) + assert_matches_type(SyncCursorPage[Graph], graph, path=["response"]) @parametrize def test_streaming_response_list(self, client: Writer) -> None: @@ -183,7 +182,7 @@ def test_streaming_response_list(self, client: Writer) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" graph = response.parse() - assert_matches_type(SyncCursorPage[GraphListResponse], graph, path=["response"]) + assert_matches_type(SyncCursorPage[Graph], graph, path=["response"]) assert cast(Any, response.is_closed) is True @@ -449,7 +448,7 @@ async def test_method_retrieve(self, async_client: AsyncWriter) -> None: graph = await async_client.graphs.retrieve( "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) - assert_matches_type(GraphRetrieveResponse, graph, path=["response"]) + assert_matches_type(Graph, graph, path=["response"]) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncWriter) -> None: @@ -460,7 +459,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncWriter) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" graph = await response.parse() - assert_matches_type(GraphRetrieveResponse, graph, path=["response"]) + assert_matches_type(Graph, graph, path=["response"]) @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncWriter) -> None: @@ -471,7 +470,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncWriter) -> N assert response.http_request.headers.get("X-Stainless-Lang") == "python" graph = await response.parse() - assert_matches_type(GraphRetrieveResponse, graph, path=["response"]) + assert_matches_type(Graph, graph, path=["response"]) assert cast(Any, response.is_closed) is True @@ -539,7 +538,7 @@ async def test_path_params_update(self, async_client: AsyncWriter) -> None: @parametrize async def test_method_list(self, async_client: AsyncWriter) -> None: graph = await async_client.graphs.list() - assert_matches_type(AsyncCursorPage[GraphListResponse], graph, path=["response"]) + assert_matches_type(AsyncCursorPage[Graph], graph, path=["response"]) @parametrize async def test_method_list_with_all_params(self, async_client: AsyncWriter) -> None: @@ -549,7 +548,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncWriter) -> N limit=0, order="asc", ) - assert_matches_type(AsyncCursorPage[GraphListResponse], graph, path=["response"]) + assert_matches_type(AsyncCursorPage[Graph], graph, path=["response"]) @parametrize async def test_raw_response_list(self, async_client: AsyncWriter) -> None: @@ -558,7 +557,7 @@ async def test_raw_response_list(self, async_client: AsyncWriter) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" graph = await response.parse() - assert_matches_type(AsyncCursorPage[GraphListResponse], graph, path=["response"]) + assert_matches_type(AsyncCursorPage[Graph], graph, path=["response"]) @parametrize async def test_streaming_response_list(self, async_client: AsyncWriter) -> None: @@ -567,7 +566,7 @@ async def test_streaming_response_list(self, async_client: AsyncWriter) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" graph = await response.parse() - assert_matches_type(AsyncCursorPage[GraphListResponse], graph, path=["response"]) + assert_matches_type(AsyncCursorPage[Graph], graph, path=["response"]) assert cast(Any, response.is_closed) is True From 67c4df3d18a47f7a047a889a128d02fd2f44744c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 19:22:51 +0000 Subject: [PATCH 299/399] chore(internal): version bump --- .release-please-manifest.json | 2 +- README.md | 4 ++-- pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 75ec52fc..97ee52d7 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.3.0" + ".": "2.3.1-rc1" } \ No newline at end of file diff --git a/README.md b/README.md index 3ecfe7d9..8e77ddea 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ The REST API documentation can be found on [dev.writer.com](https://dev.writer.c ```sh # install from PyPI -pip install writer-sdk +pip install --pre writer-sdk ``` ## Usage @@ -89,7 +89,7 @@ You can enable this by installing `aiohttp`: ```sh # install from PyPI -pip install writer-sdk[aiohttp] +pip install --pre writer-sdk[aiohttp] ``` Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: diff --git a/pyproject.toml b/pyproject.toml index 50f87e6c..7dc718e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "2.3.0" +version = "2.3.1-rc1" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index db468bf9..b3ee468c 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "2.3.0" # x-release-please-version +__version__ = "2.3.1-rc1" # x-release-please-version From 41ab59c14eec032c6b59d7dbae556885bc83c6fa Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 20 Aug 2025 13:12:08 +0000 Subject: [PATCH 300/399] chore(internal): version bump --- .release-please-manifest.json | 2 +- README.md | 4 ++-- pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 97ee52d7..2d96c4d3 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.3.1-rc1" + ".": "2.3.1" } \ No newline at end of file diff --git a/README.md b/README.md index 8e77ddea..3ecfe7d9 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ The REST API documentation can be found on [dev.writer.com](https://dev.writer.c ```sh # install from PyPI -pip install --pre writer-sdk +pip install writer-sdk ``` ## Usage @@ -89,7 +89,7 @@ You can enable this by installing `aiohttp`: ```sh # install from PyPI -pip install --pre writer-sdk[aiohttp] +pip install writer-sdk[aiohttp] ``` Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: diff --git a/pyproject.toml b/pyproject.toml index 7dc718e5..517d32cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "2.3.1-rc1" +version = "2.3.1" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index b3ee468c..fdf45740 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "2.3.1-rc1" # x-release-please-version +__version__ = "2.3.1" # x-release-please-version From 0b6606d0dfb3e30139db0963859a0ec095a5b34d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 21 Aug 2025 19:30:40 +0000 Subject: [PATCH 301/399] chore: update github action --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index beb8dd8c..d85cbb49 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: run: ./scripts/lint build: - if: github.repository == 'stainless-sdks/writer-python' && (github.event_name == 'push' || github.event.pull_request.head.repo.fork) + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork timeout-minutes: 10 name: build permissions: @@ -61,12 +61,14 @@ jobs: run: rye build - name: Get GitHub OIDC Token + if: github.repository == 'stainless-sdks/writer-python' id: github-oidc uses: actions/github-script@v6 with: script: core.setOutput('github_token', await core.getIDToken()); - name: Upload tarball + if: github.repository == 'stainless-sdks/writer-python' env: URL: https://pkg.stainless.com/s AUTH: ${{ steps.github-oidc.outputs.github_token }} From a8c044aa764fad92ce64ea3001811f3a142b2196 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 25 Aug 2025 22:26:10 +0000 Subject: [PATCH 302/399] chore(internal): change ci workflow machines --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d85cbb49..b1e32a40 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: permissions: contents: read id-token: write - runs-on: depot-ubuntu-24.04 + runs-on: ${{ github.repository == 'stainless-sdks/writer-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - uses: actions/checkout@v4 From 0d2dbfaa0f14bef25ca53d685eff0df9b2566167 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 26 Aug 2025 16:03:16 +0000 Subject: [PATCH 303/399] fix: avoid newer type syntax --- src/writerai/_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/writerai/_models.py b/src/writerai/_models.py index b8387ce9..92f7c10b 100644 --- a/src/writerai/_models.py +++ b/src/writerai/_models.py @@ -304,7 +304,7 @@ def model_dump( exclude_none=exclude_none, ) - return cast(dict[str, Any], json_safe(dumped)) if mode == "json" else dumped + return cast("dict[str, Any]", json_safe(dumped)) if mode == "json" else dumped @override def model_dump_json( From 395ed0ee1de921ebaf77c18f59e580ec5c626340 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 26 Aug 2025 21:55:42 +0000 Subject: [PATCH 304/399] chore(internal): update pyright exclude list --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 517d32cf..c8546583 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -148,6 +148,7 @@ exclude = [ "_dev", ".venv", ".nox", + ".git", ] reportImplicitOverride = true From b840aaec6eb75f1b145bc7c47734738662c893bd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 29 Aug 2025 19:19:34 +0000 Subject: [PATCH 305/399] chore(internal): add Sequence related utils --- src/writerai/_types.py | 36 ++++++++++++++++++++++++++++++++- src/writerai/_utils/__init__.py | 1 + src/writerai/_utils/_typing.py | 5 +++++ tests/utils.py | 10 ++++++++- 4 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/writerai/_types.py b/src/writerai/_types.py index cb87a472..31170df3 100644 --- a/src/writerai/_types.py +++ b/src/writerai/_types.py @@ -13,10 +13,21 @@ Mapping, TypeVar, Callable, + Iterator, Optional, Sequence, ) -from typing_extensions import Set, Literal, Protocol, TypeAlias, TypedDict, override, runtime_checkable +from typing_extensions import ( + Set, + Literal, + Protocol, + TypeAlias, + TypedDict, + SupportsIndex, + overload, + override, + runtime_checkable, +) import httpx import pydantic @@ -217,3 +228,26 @@ class _GenericAlias(Protocol): class HttpxSendArgs(TypedDict, total=False): auth: httpx.Auth follow_redirects: bool + + +_T_co = TypeVar("_T_co", covariant=True) + + +if TYPE_CHECKING: + # This works because str.__contains__ does not accept object (either in typeshed or at runtime) + # https://github.com/hauntsaninja/useful_types/blob/5e9710f3875107d068e7679fd7fec9cfab0eff3b/useful_types/__init__.py#L285 + class SequenceNotStr(Protocol[_T_co]): + @overload + def __getitem__(self, index: SupportsIndex, /) -> _T_co: ... + @overload + def __getitem__(self, index: slice, /) -> Sequence[_T_co]: ... + def __contains__(self, value: object, /) -> bool: ... + def __len__(self) -> int: ... + def __iter__(self) -> Iterator[_T_co]: ... + def index(self, value: Any, start: int = 0, stop: int = ..., /) -> int: ... + def count(self, value: Any, /) -> int: ... + def __reversed__(self) -> Iterator[_T_co]: ... +else: + # just point this to a normal `Sequence` at runtime to avoid having to special case + # deserializing our custom sequence type + SequenceNotStr = Sequence diff --git a/src/writerai/_utils/__init__.py b/src/writerai/_utils/__init__.py index d4fda26f..ca547ce5 100644 --- a/src/writerai/_utils/__init__.py +++ b/src/writerai/_utils/__init__.py @@ -38,6 +38,7 @@ extract_type_arg as extract_type_arg, is_iterable_type as is_iterable_type, is_required_type as is_required_type, + is_sequence_type as is_sequence_type, is_annotated_type as is_annotated_type, is_type_alias_type as is_type_alias_type, strip_annotated_type as strip_annotated_type, diff --git a/src/writerai/_utils/_typing.py b/src/writerai/_utils/_typing.py index 1bac9542..845cd6b2 100644 --- a/src/writerai/_utils/_typing.py +++ b/src/writerai/_utils/_typing.py @@ -26,6 +26,11 @@ def is_list_type(typ: type) -> bool: return (get_origin(typ) or typ) == list +def is_sequence_type(typ: type) -> bool: + origin = get_origin(typ) or typ + return origin == typing_extensions.Sequence or origin == typing.Sequence or origin == _c_abc.Sequence + + def is_iterable_type(typ: type) -> bool: """If the given type is `typing.Iterable[T]`""" origin = get_origin(typ) or typ diff --git a/tests/utils.py b/tests/utils.py index 64664db4..c944be62 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -4,7 +4,7 @@ import inspect import traceback import contextlib -from typing import Any, TypeVar, Iterator, cast +from typing import Any, TypeVar, Iterator, Sequence, cast from datetime import date, datetime from typing_extensions import Literal, get_args, get_origin, assert_type @@ -15,6 +15,7 @@ is_list_type, is_union_type, extract_type_arg, + is_sequence_type, is_annotated_type, is_type_alias_type, ) @@ -71,6 +72,13 @@ def assert_matches_type( if is_list_type(type_): return _assert_list_type(type_, value) + if is_sequence_type(type_): + assert isinstance(value, Sequence) + inner_type = get_args(type_)[0] + for entry in value: # type: ignore + assert_type(inner_type, entry) # type: ignore + return + if origin == str: assert isinstance(value, str) elif origin == int: From c787f6670dae4d112c4ab61addf4b8a9f0989e04 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 15:15:25 +0000 Subject: [PATCH 306/399] feat(types): replace List[str] with SequenceNotStr in params --- src/writerai/_utils/_transform.py | 6 ++++++ src/writerai/resources/applications/graphs.py | 8 +++----- src/writerai/resources/chat.py | 20 +++++++++---------- src/writerai/resources/completions.py | 20 +++++++++---------- src/writerai/resources/files.py | 7 +++---- src/writerai/resources/graphs.py | 20 +++++++++---------- src/writerai/resources/tools/tools.py | 12 +++++------ .../application_generate_content_params.py | 6 ++++-- .../types/applications/graph_update_params.py | 5 +++-- .../types/applications/job_create_params.py | 6 ++++-- src/writerai/types/chat_chat_params.py | 5 +++-- .../types/completion_create_params.py | 6 ++++-- src/writerai/types/file_retry_params.py | 5 +++-- src/writerai/types/graph_question_params.py | 6 ++++-- src/writerai/types/graph_update_params.py | 6 ++++-- .../types/shared_params/tool_param.py | 9 +++++---- src/writerai/types/tool_web_search_params.py | 8 +++++--- 17 files changed, 87 insertions(+), 68 deletions(-) diff --git a/src/writerai/_utils/_transform.py b/src/writerai/_utils/_transform.py index b0cc20a7..f0bcefd4 100644 --- a/src/writerai/_utils/_transform.py +++ b/src/writerai/_utils/_transform.py @@ -16,6 +16,7 @@ lru_cache, is_mapping, is_iterable, + is_sequence, ) from .._files import is_base64_file_input from ._typing import ( @@ -24,6 +25,7 @@ extract_type_arg, is_iterable_type, is_required_type, + is_sequence_type, is_annotated_type, strip_annotated_type, ) @@ -184,6 +186,8 @@ def _transform_recursive( (is_list_type(stripped_type) and is_list(data)) # Iterable[T] or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str)) + # Sequence[T] + or (is_sequence_type(stripped_type) and is_sequence(data) and not isinstance(data, str)) ): # dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually # intended as an iterable, so we don't transform it. @@ -346,6 +350,8 @@ async def _async_transform_recursive( (is_list_type(stripped_type) and is_list(data)) # Iterable[T] or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str)) + # Sequence[T] + or (is_sequence_type(stripped_type) and is_sequence(data) and not isinstance(data, str)) ): # dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually # intended as an iterable, so we don't transform it. diff --git a/src/writerai/resources/applications/graphs.py b/src/writerai/resources/applications/graphs.py index 9ab86c4e..cff8d7c6 100644 --- a/src/writerai/resources/applications/graphs.py +++ b/src/writerai/resources/applications/graphs.py @@ -2,11 +2,9 @@ from __future__ import annotations -from typing import List - import httpx -from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven, SequenceNotStr from ..._utils import maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource @@ -47,7 +45,7 @@ def update( self, application_id: str, *, - graph_ids: List[str], + graph_ids: SequenceNotStr[str], # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -141,7 +139,7 @@ async def update( self, application_id: str, *, - graph_ids: List[str], + graph_ids: SequenceNotStr[str], # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, diff --git a/src/writerai/resources/chat.py b/src/writerai/resources/chat.py index 85ee1464..d209a84e 100644 --- a/src/writerai/resources/chat.py +++ b/src/writerai/resources/chat.py @@ -2,13 +2,13 @@ from __future__ import annotations -from typing import List, Union, Iterable +from typing import Union, Iterable from typing_extensions import Literal, overload import httpx from ..types import chat_chat_params -from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, SequenceNotStr from .._utils import required_args, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -57,7 +57,7 @@ def chat( max_tokens: int | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, response_format: chat_chat_params.ResponseFormat | NotGiven = NOT_GIVEN, - stop: Union[List[str], str] | NotGiven = NOT_GIVEN, + stop: Union[SequenceNotStr[str], str] | NotGiven = NOT_GIVEN, stream: Literal[False] | NotGiven = NOT_GIVEN, stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, @@ -165,7 +165,7 @@ def chat( max_tokens: int | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, response_format: chat_chat_params.ResponseFormat | NotGiven = NOT_GIVEN, - stop: Union[List[str], str] | NotGiven = NOT_GIVEN, + stop: Union[SequenceNotStr[str], str] | NotGiven = NOT_GIVEN, stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, @@ -272,7 +272,7 @@ def chat( max_tokens: int | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, response_format: chat_chat_params.ResponseFormat | NotGiven = NOT_GIVEN, - stop: Union[List[str], str] | NotGiven = NOT_GIVEN, + stop: Union[SequenceNotStr[str], str] | NotGiven = NOT_GIVEN, stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, @@ -378,7 +378,7 @@ def chat( max_tokens: int | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, response_format: chat_chat_params.ResponseFormat | NotGiven = NOT_GIVEN, - stop: Union[List[str], str] | NotGiven = NOT_GIVEN, + stop: Union[SequenceNotStr[str], str] | NotGiven = NOT_GIVEN, stream: Literal[False] | Literal[True] | NotGiven = NOT_GIVEN, stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, @@ -451,7 +451,7 @@ async def chat( max_tokens: int | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, response_format: chat_chat_params.ResponseFormat | NotGiven = NOT_GIVEN, - stop: Union[List[str], str] | NotGiven = NOT_GIVEN, + stop: Union[SequenceNotStr[str], str] | NotGiven = NOT_GIVEN, stream: Literal[False] | NotGiven = NOT_GIVEN, stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, @@ -559,7 +559,7 @@ async def chat( max_tokens: int | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, response_format: chat_chat_params.ResponseFormat | NotGiven = NOT_GIVEN, - stop: Union[List[str], str] | NotGiven = NOT_GIVEN, + stop: Union[SequenceNotStr[str], str] | NotGiven = NOT_GIVEN, stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, @@ -666,7 +666,7 @@ async def chat( max_tokens: int | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, response_format: chat_chat_params.ResponseFormat | NotGiven = NOT_GIVEN, - stop: Union[List[str], str] | NotGiven = NOT_GIVEN, + stop: Union[SequenceNotStr[str], str] | NotGiven = NOT_GIVEN, stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, @@ -772,7 +772,7 @@ async def chat( max_tokens: int | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, response_format: chat_chat_params.ResponseFormat | NotGiven = NOT_GIVEN, - stop: Union[List[str], str] | NotGiven = NOT_GIVEN, + stop: Union[SequenceNotStr[str], str] | NotGiven = NOT_GIVEN, stream: Literal[False] | Literal[True] | NotGiven = NOT_GIVEN, stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, diff --git a/src/writerai/resources/completions.py b/src/writerai/resources/completions.py index 081ec326..417eecc9 100644 --- a/src/writerai/resources/completions.py +++ b/src/writerai/resources/completions.py @@ -2,13 +2,13 @@ from __future__ import annotations -from typing import List, Union +from typing import Union from typing_extensions import Literal, overload import httpx from ..types import completion_create_params -from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, SequenceNotStr from .._utils import required_args, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -55,7 +55,7 @@ def create( best_of: int | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, random_seed: int | NotGiven = NOT_GIVEN, - stop: Union[List[str], str] | NotGiven = NOT_GIVEN, + stop: Union[SequenceNotStr[str], str] | NotGiven = NOT_GIVEN, stream: Literal[False] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, @@ -121,7 +121,7 @@ def create( best_of: int | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, random_seed: int | NotGiven = NOT_GIVEN, - stop: Union[List[str], str] | NotGiven = NOT_GIVEN, + stop: Union[SequenceNotStr[str], str] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -186,7 +186,7 @@ def create( best_of: int | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, random_seed: int | NotGiven = NOT_GIVEN, - stop: Union[List[str], str] | NotGiven = NOT_GIVEN, + stop: Union[SequenceNotStr[str], str] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -250,7 +250,7 @@ def create( best_of: int | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, random_seed: int | NotGiven = NOT_GIVEN, - stop: Union[List[str], str] | NotGiven = NOT_GIVEN, + stop: Union[SequenceNotStr[str], str] | NotGiven = NOT_GIVEN, stream: Literal[False] | Literal[True] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, @@ -317,7 +317,7 @@ async def create( best_of: int | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, random_seed: int | NotGiven = NOT_GIVEN, - stop: Union[List[str], str] | NotGiven = NOT_GIVEN, + stop: Union[SequenceNotStr[str], str] | NotGiven = NOT_GIVEN, stream: Literal[False] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, @@ -383,7 +383,7 @@ async def create( best_of: int | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, random_seed: int | NotGiven = NOT_GIVEN, - stop: Union[List[str], str] | NotGiven = NOT_GIVEN, + stop: Union[SequenceNotStr[str], str] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -448,7 +448,7 @@ async def create( best_of: int | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, random_seed: int | NotGiven = NOT_GIVEN, - stop: Union[List[str], str] | NotGiven = NOT_GIVEN, + stop: Union[SequenceNotStr[str], str] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -512,7 +512,7 @@ async def create( best_of: int | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, random_seed: int | NotGiven = NOT_GIVEN, - stop: Union[List[str], str] | NotGiven = NOT_GIVEN, + stop: Union[SequenceNotStr[str], str] | NotGiven = NOT_GIVEN, stream: Literal[False] | Literal[True] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, top_p: float | NotGiven = NOT_GIVEN, diff --git a/src/writerai/resources/files.py b/src/writerai/resources/files.py index 4e633bda..458a1da5 100644 --- a/src/writerai/resources/files.py +++ b/src/writerai/resources/files.py @@ -2,13 +2,12 @@ from __future__ import annotations -from typing import List from typing_extensions import Literal import httpx from ..types import file_list_params, file_retry_params, file_upload_params -from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes +from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes, SequenceNotStr from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -236,7 +235,7 @@ def download( def retry( self, *, - file_ids: List[str], + file_ids: SequenceNotStr[str], # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -507,7 +506,7 @@ async def download( async def retry( self, *, - file_ids: List[str], + file_ids: SequenceNotStr[str], # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, diff --git a/src/writerai/resources/graphs.py b/src/writerai/resources/graphs.py index a427bb93..e3fe5cac 100644 --- a/src/writerai/resources/graphs.py +++ b/src/writerai/resources/graphs.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import List, Iterable +from typing import Iterable from typing_extensions import Literal, overload import httpx @@ -14,7 +14,7 @@ graph_question_params, graph_add_file_to_graph_params, ) -from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, SequenceNotStr from .._utils import required_args, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -324,7 +324,7 @@ def add_file_to_graph( def question( self, *, - graph_ids: List[str], + graph_ids: SequenceNotStr[str], question: str, stream: Literal[False] | NotGiven = NOT_GIVEN, subqueries: bool | NotGiven = NOT_GIVEN, @@ -363,7 +363,7 @@ def question( def question( self, *, - graph_ids: List[str], + graph_ids: SequenceNotStr[str], question: str, stream: Literal[True], subqueries: bool | NotGiven = NOT_GIVEN, @@ -402,7 +402,7 @@ def question( def question( self, *, - graph_ids: List[str], + graph_ids: SequenceNotStr[str], question: str, stream: bool, subqueries: bool | NotGiven = NOT_GIVEN, @@ -441,7 +441,7 @@ def question( def question( self, *, - graph_ids: List[str], + graph_ids: SequenceNotStr[str], question: str, stream: Literal[False] | Literal[True] | NotGiven = NOT_GIVEN, subqueries: bool | NotGiven = NOT_GIVEN, @@ -797,7 +797,7 @@ async def add_file_to_graph( async def question( self, *, - graph_ids: List[str], + graph_ids: SequenceNotStr[str], question: str, stream: Literal[False] | NotGiven = NOT_GIVEN, subqueries: bool | NotGiven = NOT_GIVEN, @@ -836,7 +836,7 @@ async def question( async def question( self, *, - graph_ids: List[str], + graph_ids: SequenceNotStr[str], question: str, stream: Literal[True], subqueries: bool | NotGiven = NOT_GIVEN, @@ -875,7 +875,7 @@ async def question( async def question( self, *, - graph_ids: List[str], + graph_ids: SequenceNotStr[str], question: str, stream: bool, subqueries: bool | NotGiven = NOT_GIVEN, @@ -914,7 +914,7 @@ async def question( async def question( self, *, - graph_ids: List[str], + graph_ids: SequenceNotStr[str], question: str, stream: Literal[False] | Literal[True] | NotGiven = NOT_GIVEN, subqueries: bool | NotGiven = NOT_GIVEN, diff --git a/src/writerai/resources/tools/tools.py b/src/writerai/resources/tools/tools.py index cab8fb75..1f739424 100644 --- a/src/writerai/resources/tools/tools.py +++ b/src/writerai/resources/tools/tools.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import List, Union +from typing import Union from typing_extensions import Literal import httpx @@ -13,7 +13,7 @@ tool_web_search_params, tool_context_aware_splitting_params, ) -from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven, SequenceNotStr from ..._utils import maybe_transform, async_maybe_transform from ..._compat import cached_property from .comprehend import ( @@ -358,9 +358,9 @@ def web_search( ] | NotGiven = NOT_GIVEN, days: int | NotGiven = NOT_GIVEN, - exclude_domains: List[str] | NotGiven = NOT_GIVEN, + exclude_domains: SequenceNotStr[str] | NotGiven = NOT_GIVEN, include_answer: bool | NotGiven = NOT_GIVEN, - include_domains: List[str] | NotGiven = NOT_GIVEN, + include_domains: SequenceNotStr[str] | NotGiven = NOT_GIVEN, include_raw_content: Union[Literal["text", "markdown"], bool] | NotGiven = NOT_GIVEN, max_results: int | NotGiven = NOT_GIVEN, query: str | NotGiven = NOT_GIVEN, @@ -776,9 +776,9 @@ async def web_search( ] | NotGiven = NOT_GIVEN, days: int | NotGiven = NOT_GIVEN, - exclude_domains: List[str] | NotGiven = NOT_GIVEN, + exclude_domains: SequenceNotStr[str] | NotGiven = NOT_GIVEN, include_answer: bool | NotGiven = NOT_GIVEN, - include_domains: List[str] | NotGiven = NOT_GIVEN, + include_domains: SequenceNotStr[str] | NotGiven = NOT_GIVEN, include_raw_content: Union[Literal["text", "markdown"], bool] | NotGiven = NOT_GIVEN, max_results: int | NotGiven = NOT_GIVEN, query: str | NotGiven = NOT_GIVEN, diff --git a/src/writerai/types/application_generate_content_params.py b/src/writerai/types/application_generate_content_params.py index bfd7011c..401d52d4 100644 --- a/src/writerai/types/application_generate_content_params.py +++ b/src/writerai/types/application_generate_content_params.py @@ -2,9 +2,11 @@ from __future__ import annotations -from typing import List, Union, Iterable +from typing import Union, Iterable from typing_extensions import Literal, Required, TypedDict +from .._types import SequenceNotStr + __all__ = [ "ApplicationGenerateContentParamsBase", "Input", @@ -26,7 +28,7 @@ class Input(TypedDict, total=False): input type. """ - value: Required[List[str]] + value: Required[SequenceNotStr[str]] """The value for the input field. If the input type is "File upload", you must pass the `file_id` of an uploaded diff --git a/src/writerai/types/applications/graph_update_params.py b/src/writerai/types/applications/graph_update_params.py index 8a3bd87c..11fcf3bf 100644 --- a/src/writerai/types/applications/graph_update_params.py +++ b/src/writerai/types/applications/graph_update_params.py @@ -2,14 +2,15 @@ from __future__ import annotations -from typing import List from typing_extensions import Required, TypedDict +from ..._types import SequenceNotStr + __all__ = ["GraphUpdateParams"] class GraphUpdateParams(TypedDict, total=False): - graph_ids: Required[List[str]] + graph_ids: Required[SequenceNotStr[str]] """A list of Knowledge Graph IDs to associate with the application. Note that this will replace the existing list of Knowledge Graphs associated diff --git a/src/writerai/types/applications/job_create_params.py b/src/writerai/types/applications/job_create_params.py index 56c84c46..f3453a9f 100644 --- a/src/writerai/types/applications/job_create_params.py +++ b/src/writerai/types/applications/job_create_params.py @@ -2,9 +2,11 @@ from __future__ import annotations -from typing import List, Iterable +from typing import Iterable from typing_extensions import Required, TypedDict +from ..._types import SequenceNotStr + __all__ = ["JobCreateParams", "Input"] @@ -22,7 +24,7 @@ class Input(TypedDict, total=False): input type. """ - value: Required[List[str]] + value: Required[SequenceNotStr[str]] """The value for the input field. If the input type is "File upload", you must pass the `file_id` of an uploaded diff --git a/src/writerai/types/chat_chat_params.py b/src/writerai/types/chat_chat_params.py index e15fb942..522248af 100644 --- a/src/writerai/types/chat_chat_params.py +++ b/src/writerai/types/chat_chat_params.py @@ -2,9 +2,10 @@ from __future__ import annotations -from typing import List, Union, Iterable, Optional +from typing import Union, Iterable, Optional from typing_extensions import Literal, Required, TypeAlias, TypedDict +from .._types import SequenceNotStr from .shared_params.tool_call import ToolCall from .shared_params.graph_data import GraphData from .shared_params.tool_param import ToolParam @@ -69,7 +70,7 @@ class ChatChatParamsBase(TypedDict, total=False): also provide a `json_schema` object. """ - stop: Union[List[str], str] + stop: Union[SequenceNotStr[str], str] """ A token or sequence of tokens that, when generated, will cause the model to stop producing further content. This can be a single token or an array of tokens, diff --git a/src/writerai/types/completion_create_params.py b/src/writerai/types/completion_create_params.py index b327c132..017e18bf 100644 --- a/src/writerai/types/completion_create_params.py +++ b/src/writerai/types/completion_create_params.py @@ -2,9 +2,11 @@ from __future__ import annotations -from typing import List, Union +from typing import Union from typing_extensions import Literal, Required, TypedDict +from .._types import SequenceNotStr + __all__ = ["CompletionCreateParamsBase", "CompletionCreateParamsNonStreaming", "CompletionCreateParamsStreaming"] @@ -35,7 +37,7 @@ class CompletionCreateParamsBase(TypedDict, total=False): reproducibility of the output when the same inputs are provided. """ - stop: Union[List[str], str] + stop: Union[SequenceNotStr[str], str] """Specifies stopping conditions for the model's output generation. This can be an array of strings or a single string that the model will look for diff --git a/src/writerai/types/file_retry_params.py b/src/writerai/types/file_retry_params.py index 2f17762c..8882af6a 100644 --- a/src/writerai/types/file_retry_params.py +++ b/src/writerai/types/file_retry_params.py @@ -2,12 +2,13 @@ from __future__ import annotations -from typing import List from typing_extensions import Required, TypedDict +from .._types import SequenceNotStr + __all__ = ["FileRetryParams"] class FileRetryParams(TypedDict, total=False): - file_ids: Required[List[str]] + file_ids: Required[SequenceNotStr[str]] """The unique identifier of the files to retry.""" diff --git a/src/writerai/types/graph_question_params.py b/src/writerai/types/graph_question_params.py index b31390b6..02e8b513 100644 --- a/src/writerai/types/graph_question_params.py +++ b/src/writerai/types/graph_question_params.py @@ -2,14 +2,16 @@ from __future__ import annotations -from typing import List, Union +from typing import Union from typing_extensions import Literal, Required, TypedDict +from .._types import SequenceNotStr + __all__ = ["GraphQuestionParamsBase", "GraphQuestionParamsNonStreaming", "GraphQuestionParamsStreaming"] class GraphQuestionParamsBase(TypedDict, total=False): - graph_ids: Required[List[str]] + graph_ids: Required[SequenceNotStr[str]] """The unique identifiers of the Knowledge Graphs to query.""" question: Required[str] diff --git a/src/writerai/types/graph_update_params.py b/src/writerai/types/graph_update_params.py index 7be9324d..f400f902 100644 --- a/src/writerai/types/graph_update_params.py +++ b/src/writerai/types/graph_update_params.py @@ -2,9 +2,11 @@ from __future__ import annotations -from typing import List, Iterable +from typing import Iterable from typing_extensions import Literal, Required, TypedDict +from .._types import SequenceNotStr + __all__ = ["GraphUpdateParams", "URL"] @@ -36,5 +38,5 @@ class URL(TypedDict, total=False): url: Required[str] """The URL to be processed by the web connector.""" - exclude_urls: List[str] + exclude_urls: SequenceNotStr[str] """An array of URLs to exclude from processing within this web connector.""" diff --git a/src/writerai/types/shared_params/tool_param.py b/src/writerai/types/shared_params/tool_param.py index 25211144..677a33bd 100644 --- a/src/writerai/types/shared_params/tool_param.py +++ b/src/writerai/types/shared_params/tool_param.py @@ -2,9 +2,10 @@ from __future__ import annotations -from typing import List, Union, Iterable +from typing import Union, Iterable from typing_extensions import Literal, Required, TypeAlias, TypedDict +from ..._types import SequenceNotStr from .function_definition import FunctionDefinition __all__ = [ @@ -33,7 +34,7 @@ class FunctionTool(TypedDict, total=False): class GraphToolFunction(TypedDict, total=False): - graph_ids: Required[List[str]] + graph_ids: Required[SequenceNotStr[str]] """An array of graph IDs to use in the tool.""" subqueries: Required[bool] @@ -161,10 +162,10 @@ class VisionTool(TypedDict, total=False): class WebSearchToolFunction(TypedDict, total=False): - exclude_domains: Required[List[str]] + exclude_domains: Required[SequenceNotStr[str]] """An array of domains to exclude from the search results.""" - include_domains: Required[List[str]] + include_domains: Required[SequenceNotStr[str]] """An array of domains to include in the search results.""" diff --git a/src/writerai/types/tool_web_search_params.py b/src/writerai/types/tool_web_search_params.py index 800dc033..6f639e02 100644 --- a/src/writerai/types/tool_web_search_params.py +++ b/src/writerai/types/tool_web_search_params.py @@ -2,9 +2,11 @@ from __future__ import annotations -from typing import List, Union +from typing import Union from typing_extensions import Literal, TypedDict +from .._types import SequenceNotStr + __all__ = ["ToolWebSearchParams"] @@ -192,7 +194,7 @@ class ToolWebSearchParams(TypedDict, total=False): days: int """For news topic searches, specifies how many days of news coverage to include.""" - exclude_domains: List[str] + exclude_domains: SequenceNotStr[str] """Domains to exclude from the search. If unset, the search includes all domains.""" include_answer: bool @@ -201,7 +203,7 @@ class ToolWebSearchParams(TypedDict, total=False): If `false`, only search results are returned. """ - include_domains: List[str] + include_domains: SequenceNotStr[str] """Domains to include in the search. If unset, the search includes all domains.""" include_raw_content: Union[Literal["text", "markdown"], bool] From 550ab178a8a190f54bc12446b760fd832aee50a5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 3 Sep 2025 21:55:03 +0000 Subject: [PATCH 307/399] feat: improve future compat with pydantic v3 --- src/writerai/_base_client.py | 6 +- src/writerai/_compat.py | 96 ++++++++--------- src/writerai/_models.py | 80 +++++++------- src/writerai/_utils/__init__.py | 10 +- src/writerai/_utils/_compat.py | 45 ++++++++ src/writerai/_utils/_datetime_parse.py | 136 ++++++++++++++++++++++++ src/writerai/_utils/_transform.py | 6 +- src/writerai/_utils/_typing.py | 2 +- src/writerai/_utils/_utils.py | 1 - tests/test_models.py | 48 ++++----- tests/test_transform.py | 16 +-- tests/test_utils/test_datetime_parse.py | 110 +++++++++++++++++++ tests/utils.py | 8 +- 13 files changed, 432 insertions(+), 132 deletions(-) create mode 100644 src/writerai/_utils/_compat.py create mode 100644 src/writerai/_utils/_datetime_parse.py create mode 100644 tests/test_utils/test_datetime_parse.py diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index 5fdf9390..d251c921 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -59,7 +59,7 @@ ModelBuilderProtocol, ) from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping -from ._compat import PYDANTIC_V2, model_copy, model_dump +from ._compat import PYDANTIC_V1, model_copy, model_dump from ._models import GenericModel, FinalRequestOptions, validate_type, construct_type from ._response import ( APIResponse, @@ -232,7 +232,7 @@ def _set_private_attributes( model: Type[_T], options: FinalRequestOptions, ) -> None: - if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None: + if (not PYDANTIC_V1) and getattr(self, "__pydantic_private__", None) is None: self.__pydantic_private__ = {} self._model = model @@ -320,7 +320,7 @@ def _set_private_attributes( client: AsyncAPIClient, options: FinalRequestOptions, ) -> None: - if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None: + if (not PYDANTIC_V1) and getattr(self, "__pydantic_private__", None) is None: self.__pydantic_private__ = {} self._model = model diff --git a/src/writerai/_compat.py b/src/writerai/_compat.py index 92d9ee61..bdef67f0 100644 --- a/src/writerai/_compat.py +++ b/src/writerai/_compat.py @@ -12,14 +12,13 @@ _T = TypeVar("_T") _ModelT = TypeVar("_ModelT", bound=pydantic.BaseModel) -# --------------- Pydantic v2 compatibility --------------- +# --------------- Pydantic v2, v3 compatibility --------------- # Pyright incorrectly reports some of our functions as overriding a method when they don't # pyright: reportIncompatibleMethodOverride=false -PYDANTIC_V2 = pydantic.VERSION.startswith("2.") +PYDANTIC_V1 = pydantic.VERSION.startswith("1.") -# v1 re-exports if TYPE_CHECKING: def parse_date(value: date | StrBytesIntFloat) -> date: # noqa: ARG001 @@ -44,90 +43,92 @@ def is_typeddict(type_: type[Any]) -> bool: # noqa: ARG001 ... else: - if PYDANTIC_V2: - from pydantic.v1.typing import ( + # v1 re-exports + if PYDANTIC_V1: + from pydantic.typing import ( get_args as get_args, is_union as is_union, get_origin as get_origin, is_typeddict as is_typeddict, is_literal_type as is_literal_type, ) - from pydantic.v1.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime + from pydantic.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime else: - from pydantic.typing import ( + from ._utils import ( get_args as get_args, is_union as is_union, get_origin as get_origin, + parse_date as parse_date, is_typeddict as is_typeddict, + parse_datetime as parse_datetime, is_literal_type as is_literal_type, ) - from pydantic.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime # refactored config if TYPE_CHECKING: from pydantic import ConfigDict as ConfigDict else: - if PYDANTIC_V2: - from pydantic import ConfigDict - else: + if PYDANTIC_V1: # TODO: provide an error message here? ConfigDict = None + else: + from pydantic import ConfigDict as ConfigDict # renamed methods / properties def parse_obj(model: type[_ModelT], value: object) -> _ModelT: - if PYDANTIC_V2: - return model.model_validate(value) - else: + if PYDANTIC_V1: return cast(_ModelT, model.parse_obj(value)) # pyright: ignore[reportDeprecated, reportUnnecessaryCast] + else: + return model.model_validate(value) def field_is_required(field: FieldInfo) -> bool: - if PYDANTIC_V2: - return field.is_required() - return field.required # type: ignore + if PYDANTIC_V1: + return field.required # type: ignore + return field.is_required() def field_get_default(field: FieldInfo) -> Any: value = field.get_default() - if PYDANTIC_V2: - from pydantic_core import PydanticUndefined - - if value == PydanticUndefined: - return None + if PYDANTIC_V1: return value + from pydantic_core import PydanticUndefined + + if value == PydanticUndefined: + return None return value def field_outer_type(field: FieldInfo) -> Any: - if PYDANTIC_V2: - return field.annotation - return field.outer_type_ # type: ignore + if PYDANTIC_V1: + return field.outer_type_ # type: ignore + return field.annotation def get_model_config(model: type[pydantic.BaseModel]) -> Any: - if PYDANTIC_V2: - return model.model_config - return model.__config__ # type: ignore + if PYDANTIC_V1: + return model.__config__ # type: ignore + return model.model_config def get_model_fields(model: type[pydantic.BaseModel]) -> dict[str, FieldInfo]: - if PYDANTIC_V2: - return model.model_fields - return model.__fields__ # type: ignore + if PYDANTIC_V1: + return model.__fields__ # type: ignore + return model.model_fields def model_copy(model: _ModelT, *, deep: bool = False) -> _ModelT: - if PYDANTIC_V2: - return model.model_copy(deep=deep) - return model.copy(deep=deep) # type: ignore + if PYDANTIC_V1: + return model.copy(deep=deep) # type: ignore + return model.model_copy(deep=deep) def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str: - if PYDANTIC_V2: - return model.model_dump_json(indent=indent) - return model.json(indent=indent) # type: ignore + if PYDANTIC_V1: + return model.json(indent=indent) # type: ignore + return model.model_dump_json(indent=indent) def model_dump( @@ -139,14 +140,14 @@ def model_dump( warnings: bool = True, mode: Literal["json", "python"] = "python", ) -> dict[str, Any]: - if PYDANTIC_V2 or hasattr(model, "model_dump"): + if (not PYDANTIC_V1) or hasattr(model, "model_dump"): return model.model_dump( mode=mode, exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, # warnings are not supported in Pydantic v1 - warnings=warnings if PYDANTIC_V2 else True, + warnings=True if PYDANTIC_V1 else warnings, ) return cast( "dict[str, Any]", @@ -159,9 +160,9 @@ def model_dump( def model_parse(model: type[_ModelT], data: Any) -> _ModelT: - if PYDANTIC_V2: - return model.model_validate(data) - return model.parse_obj(data) # pyright: ignore[reportDeprecated] + if PYDANTIC_V1: + return model.parse_obj(data) # pyright: ignore[reportDeprecated] + return model.model_validate(data) # generic models @@ -170,17 +171,16 @@ def model_parse(model: type[_ModelT], data: Any) -> _ModelT: class GenericModel(pydantic.BaseModel): ... else: - if PYDANTIC_V2: + if PYDANTIC_V1: + import pydantic.generics + + class GenericModel(pydantic.generics.GenericModel, pydantic.BaseModel): ... + else: # there no longer needs to be a distinction in v2 but # we still have to create our own subclass to avoid # inconsistent MRO ordering errors class GenericModel(pydantic.BaseModel): ... - else: - import pydantic.generics - - class GenericModel(pydantic.generics.GenericModel, pydantic.BaseModel): ... - # cached properties if TYPE_CHECKING: diff --git a/src/writerai/_models.py b/src/writerai/_models.py index 92f7c10b..3a6017ef 100644 --- a/src/writerai/_models.py +++ b/src/writerai/_models.py @@ -50,7 +50,7 @@ strip_annotated_type, ) from ._compat import ( - PYDANTIC_V2, + PYDANTIC_V1, ConfigDict, GenericModel as BaseGenericModel, get_args, @@ -81,11 +81,7 @@ class _ConfigProtocol(Protocol): class BaseModel(pydantic.BaseModel): - if PYDANTIC_V2: - model_config: ClassVar[ConfigDict] = ConfigDict( - extra="allow", defer_build=coerce_boolean(os.environ.get("DEFER_PYDANTIC_BUILD", "true")) - ) - else: + if PYDANTIC_V1: @property @override @@ -95,6 +91,10 @@ def model_fields_set(self) -> set[str]: class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated] extra: Any = pydantic.Extra.allow # type: ignore + else: + model_config: ClassVar[ConfigDict] = ConfigDict( + extra="allow", defer_build=coerce_boolean(os.environ.get("DEFER_PYDANTIC_BUILD", "true")) + ) def to_dict( self, @@ -215,25 +215,25 @@ def construct( # pyright: ignore[reportIncompatibleMethodOverride] if key not in model_fields: parsed = construct_type(value=value, type_=extra_field_type) if extra_field_type is not None else value - if PYDANTIC_V2: - _extra[key] = parsed - else: + if PYDANTIC_V1: _fields_set.add(key) fields_values[key] = parsed + else: + _extra[key] = parsed object.__setattr__(m, "__dict__", fields_values) - if PYDANTIC_V2: - # these properties are copied from Pydantic's `model_construct()` method - object.__setattr__(m, "__pydantic_private__", None) - object.__setattr__(m, "__pydantic_extra__", _extra) - object.__setattr__(m, "__pydantic_fields_set__", _fields_set) - else: + if PYDANTIC_V1: # init_private_attributes() does not exist in v2 m._init_private_attributes() # type: ignore # copied from Pydantic v1's `construct()` method object.__setattr__(m, "__fields_set__", _fields_set) + else: + # these properties are copied from Pydantic's `model_construct()` method + object.__setattr__(m, "__pydantic_private__", None) + object.__setattr__(m, "__pydantic_extra__", _extra) + object.__setattr__(m, "__pydantic_fields_set__", _fields_set) return m @@ -243,7 +243,7 @@ def construct( # pyright: ignore[reportIncompatibleMethodOverride] # although not in practice model_construct = construct - if not PYDANTIC_V2: + if PYDANTIC_V1: # we define aliases for some of the new pydantic v2 methods so # that we can just document these methods without having to specify # a specific pydantic version as some users may not know which @@ -363,10 +363,10 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object: if value is None: return field_get_default(field) - if PYDANTIC_V2: - type_ = field.annotation - else: + if PYDANTIC_V1: type_ = cast(type, field.outer_type_) # type: ignore + else: + type_ = field.annotation # type: ignore if type_ is None: raise RuntimeError(f"Unexpected field type is None for {key}") @@ -375,7 +375,7 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object: def _get_extra_fields_type(cls: type[pydantic.BaseModel]) -> type | None: - if not PYDANTIC_V2: + if PYDANTIC_V1: # TODO return None @@ -628,30 +628,30 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, for variant in get_args(union): variant = strip_annotated_type(variant) if is_basemodel_type(variant): - if PYDANTIC_V2: - field = _extract_field_schema_pv2(variant, discriminator_field_name) - if not field: + if PYDANTIC_V1: + field_info = cast("dict[str, FieldInfo]", variant.__fields__).get(discriminator_field_name) # pyright: ignore[reportDeprecated, reportUnnecessaryCast] + if not field_info: continue # Note: if one variant defines an alias then they all should - discriminator_alias = field.get("serialization_alias") - - field_schema = field["schema"] + discriminator_alias = field_info.alias - if field_schema["type"] == "literal": - for entry in cast("LiteralSchema", field_schema)["expected"]: + if (annotation := getattr(field_info, "annotation", None)) and is_literal_type(annotation): + for entry in get_args(annotation): if isinstance(entry, str): mapping[entry] = variant else: - field_info = cast("dict[str, FieldInfo]", variant.__fields__).get(discriminator_field_name) # pyright: ignore[reportDeprecated, reportUnnecessaryCast] - if not field_info: + field = _extract_field_schema_pv2(variant, discriminator_field_name) + if not field: continue # Note: if one variant defines an alias then they all should - discriminator_alias = field_info.alias + discriminator_alias = field.get("serialization_alias") - if (annotation := getattr(field_info, "annotation", None)) and is_literal_type(annotation): - for entry in get_args(annotation): + field_schema = field["schema"] + + if field_schema["type"] == "literal": + for entry in cast("LiteralSchema", field_schema)["expected"]: if isinstance(entry, str): mapping[entry] = variant @@ -714,7 +714,7 @@ class GenericModel(BaseGenericModel, BaseModel): pass -if PYDANTIC_V2: +if not PYDANTIC_V1: from pydantic import TypeAdapter as _TypeAdapter _CachedTypeAdapter = cast("TypeAdapter[object]", lru_cache(maxsize=None)(_TypeAdapter)) @@ -782,12 +782,12 @@ class FinalRequestOptions(pydantic.BaseModel): json_data: Union[Body, None] = None extra_json: Union[AnyMapping, None] = None - if PYDANTIC_V2: - model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True) - else: + if PYDANTIC_V1: class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated] arbitrary_types_allowed: bool = True + else: + model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True) def get_max_retries(self, max_retries: int) -> int: if isinstance(self.max_retries, NotGiven): @@ -820,9 +820,9 @@ def construct( # type: ignore key: strip_not_given(value) for key, value in values.items() } - if PYDANTIC_V2: - return super().model_construct(_fields_set, **kwargs) - return cast(FinalRequestOptions, super().construct(_fields_set, **kwargs)) # pyright: ignore[reportDeprecated] + if PYDANTIC_V1: + return cast(FinalRequestOptions, super().construct(_fields_set, **kwargs)) # pyright: ignore[reportDeprecated] + return super().model_construct(_fields_set, **kwargs) if not TYPE_CHECKING: # type checkers incorrectly complain about this assignment diff --git a/src/writerai/_utils/__init__.py b/src/writerai/_utils/__init__.py index ca547ce5..dc64e29a 100644 --- a/src/writerai/_utils/__init__.py +++ b/src/writerai/_utils/__init__.py @@ -10,7 +10,6 @@ lru_cache as lru_cache, is_mapping as is_mapping, is_tuple_t as is_tuple_t, - parse_date as parse_date, is_iterable as is_iterable, is_sequence as is_sequence, coerce_float as coerce_float, @@ -23,7 +22,6 @@ coerce_boolean as coerce_boolean, coerce_integer as coerce_integer, file_from_path as file_from_path, - parse_datetime as parse_datetime, strip_not_given as strip_not_given, deepcopy_minimal as deepcopy_minimal, get_async_library as get_async_library, @@ -32,6 +30,13 @@ maybe_coerce_boolean as maybe_coerce_boolean, maybe_coerce_integer as maybe_coerce_integer, ) +from ._compat import ( + get_args as get_args, + is_union as is_union, + get_origin as get_origin, + is_typeddict as is_typeddict, + is_literal_type as is_literal_type, +) from ._typing import ( is_list_type as is_list_type, is_union_type as is_union_type, @@ -56,3 +61,4 @@ function_has_argument as function_has_argument, assert_signatures_in_sync as assert_signatures_in_sync, ) +from ._datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime diff --git a/src/writerai/_utils/_compat.py b/src/writerai/_utils/_compat.py new file mode 100644 index 00000000..dd703233 --- /dev/null +++ b/src/writerai/_utils/_compat.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import sys +import typing_extensions +from typing import Any, Type, Union, Literal, Optional +from datetime import date, datetime +from typing_extensions import get_args as _get_args, get_origin as _get_origin + +from .._types import StrBytesIntFloat +from ._datetime_parse import parse_date as _parse_date, parse_datetime as _parse_datetime + +_LITERAL_TYPES = {Literal, typing_extensions.Literal} + + +def get_args(tp: type[Any]) -> tuple[Any, ...]: + return _get_args(tp) + + +def get_origin(tp: type[Any]) -> type[Any] | None: + return _get_origin(tp) + + +def is_union(tp: Optional[Type[Any]]) -> bool: + if sys.version_info < (3, 10): + return tp is Union # type: ignore[comparison-overlap] + else: + import types + + return tp is Union or tp is types.UnionType + + +def is_typeddict(tp: Type[Any]) -> bool: + return typing_extensions.is_typeddict(tp) + + +def is_literal_type(tp: Type[Any]) -> bool: + return get_origin(tp) in _LITERAL_TYPES + + +def parse_date(value: Union[date, StrBytesIntFloat]) -> date: + return _parse_date(value) + + +def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime: + return _parse_datetime(value) diff --git a/src/writerai/_utils/_datetime_parse.py b/src/writerai/_utils/_datetime_parse.py new file mode 100644 index 00000000..7cb9d9e6 --- /dev/null +++ b/src/writerai/_utils/_datetime_parse.py @@ -0,0 +1,136 @@ +""" +This file contains code from https://github.com/pydantic/pydantic/blob/main/pydantic/v1/datetime_parse.py +without the Pydantic v1 specific errors. +""" + +from __future__ import annotations + +import re +from typing import Dict, Union, Optional +from datetime import date, datetime, timezone, timedelta + +from .._types import StrBytesIntFloat + +date_expr = r"(?P\d{4})-(?P\d{1,2})-(?P\d{1,2})" +time_expr = ( + r"(?P\d{1,2}):(?P\d{1,2})" + r"(?::(?P\d{1,2})(?:\.(?P\d{1,6})\d{0,6})?)?" + r"(?PZ|[+-]\d{2}(?::?\d{2})?)?$" +) + +date_re = re.compile(f"{date_expr}$") +datetime_re = re.compile(f"{date_expr}[T ]{time_expr}") + + +EPOCH = datetime(1970, 1, 1) +# if greater than this, the number is in ms, if less than or equal it's in seconds +# (in seconds this is 11th October 2603, in ms it's 20th August 1970) +MS_WATERSHED = int(2e10) +# slightly more than datetime.max in ns - (datetime.max - EPOCH).total_seconds() * 1e9 +MAX_NUMBER = int(3e20) + + +def _get_numeric(value: StrBytesIntFloat, native_expected_type: str) -> Union[None, int, float]: + if isinstance(value, (int, float)): + return value + try: + return float(value) + except ValueError: + return None + except TypeError: + raise TypeError(f"invalid type; expected {native_expected_type}, string, bytes, int or float") from None + + +def _from_unix_seconds(seconds: Union[int, float]) -> datetime: + if seconds > MAX_NUMBER: + return datetime.max + elif seconds < -MAX_NUMBER: + return datetime.min + + while abs(seconds) > MS_WATERSHED: + seconds /= 1000 + dt = EPOCH + timedelta(seconds=seconds) + return dt.replace(tzinfo=timezone.utc) + + +def _parse_timezone(value: Optional[str]) -> Union[None, int, timezone]: + if value == "Z": + return timezone.utc + elif value is not None: + offset_mins = int(value[-2:]) if len(value) > 3 else 0 + offset = 60 * int(value[1:3]) + offset_mins + if value[0] == "-": + offset = -offset + return timezone(timedelta(minutes=offset)) + else: + return None + + +def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime: + """ + Parse a datetime/int/float/string and return a datetime.datetime. + + This function supports time zone offsets. When the input contains one, + the output uses a timezone with a fixed offset from UTC. + + Raise ValueError if the input is well formatted but not a valid datetime. + Raise ValueError if the input isn't well formatted. + """ + if isinstance(value, datetime): + return value + + number = _get_numeric(value, "datetime") + if number is not None: + return _from_unix_seconds(number) + + if isinstance(value, bytes): + value = value.decode() + + assert not isinstance(value, (float, int)) + + match = datetime_re.match(value) + if match is None: + raise ValueError("invalid datetime format") + + kw = match.groupdict() + if kw["microsecond"]: + kw["microsecond"] = kw["microsecond"].ljust(6, "0") + + tzinfo = _parse_timezone(kw.pop("tzinfo")) + kw_: Dict[str, Union[None, int, timezone]] = {k: int(v) for k, v in kw.items() if v is not None} + kw_["tzinfo"] = tzinfo + + return datetime(**kw_) # type: ignore + + +def parse_date(value: Union[date, StrBytesIntFloat]) -> date: + """ + Parse a date/int/float/string and return a datetime.date. + + Raise ValueError if the input is well formatted but not a valid date. + Raise ValueError if the input isn't well formatted. + """ + if isinstance(value, date): + if isinstance(value, datetime): + return value.date() + else: + return value + + number = _get_numeric(value, "date") + if number is not None: + return _from_unix_seconds(number).date() + + if isinstance(value, bytes): + value = value.decode() + + assert not isinstance(value, (float, int)) + match = date_re.match(value) + if match is None: + raise ValueError("invalid date format") + + kw = {k: int(v) for k, v in match.groupdict().items()} + + try: + return date(**kw) + except ValueError: + raise ValueError("invalid date format") from None diff --git a/src/writerai/_utils/_transform.py b/src/writerai/_utils/_transform.py index f0bcefd4..c19124f0 100644 --- a/src/writerai/_utils/_transform.py +++ b/src/writerai/_utils/_transform.py @@ -19,6 +19,7 @@ is_sequence, ) from .._files import is_base64_file_input +from ._compat import get_origin, is_typeddict from ._typing import ( is_list_type, is_union_type, @@ -29,7 +30,6 @@ is_annotated_type, strip_annotated_type, ) -from .._compat import get_origin, model_dump, is_typeddict _T = TypeVar("_T") @@ -169,6 +169,8 @@ def _transform_recursive( Defaults to the same value as the `annotation` argument. """ + from .._compat import model_dump + if inner_type is None: inner_type = annotation @@ -333,6 +335,8 @@ async def _async_transform_recursive( Defaults to the same value as the `annotation` argument. """ + from .._compat import model_dump + if inner_type is None: inner_type = annotation diff --git a/src/writerai/_utils/_typing.py b/src/writerai/_utils/_typing.py index 845cd6b2..193109f3 100644 --- a/src/writerai/_utils/_typing.py +++ b/src/writerai/_utils/_typing.py @@ -15,7 +15,7 @@ from ._utils import lru_cache from .._types import InheritsGeneric -from .._compat import is_union as _is_union +from ._compat import is_union as _is_union def is_annotated_type(typ: type) -> bool: diff --git a/src/writerai/_utils/_utils.py b/src/writerai/_utils/_utils.py index ea3cf3f2..f0818595 100644 --- a/src/writerai/_utils/_utils.py +++ b/src/writerai/_utils/_utils.py @@ -22,7 +22,6 @@ import sniffio from .._types import NotGiven, FileTypes, NotGivenOr, HeadersLike -from .._compat import parse_date as parse_date, parse_datetime as parse_datetime _T = TypeVar("_T") _TupleT = TypeVar("_TupleT", bound=Tuple[object, ...]) diff --git a/tests/test_models.py b/tests/test_models.py index 0bf7e815..af9b6e48 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -8,7 +8,7 @@ from pydantic import Field from writerai._utils import PropertyInfo -from writerai._compat import PYDANTIC_V2, parse_obj, model_dump, model_json +from writerai._compat import PYDANTIC_V1, parse_obj, model_dump, model_json from writerai._models import BaseModel, construct_type @@ -294,12 +294,12 @@ class Model(BaseModel): assert cast(bool, m.foo) is True m = Model.construct(foo={"name": 3}) - if PYDANTIC_V2: - assert isinstance(m.foo, Submodel1) - assert m.foo.name == 3 # type: ignore - else: + if PYDANTIC_V1: assert isinstance(m.foo, Submodel2) assert m.foo.name == "3" + else: + assert isinstance(m.foo, Submodel1) + assert m.foo.name == 3 # type: ignore def test_list_of_unions() -> None: @@ -426,10 +426,10 @@ class Model(BaseModel): expected = datetime(2019, 12, 27, 18, 11, 19, 117000, tzinfo=timezone.utc) - if PYDANTIC_V2: - expected_json = '{"created_at":"2019-12-27T18:11:19.117000Z"}' - else: + if PYDANTIC_V1: expected_json = '{"created_at": "2019-12-27T18:11:19.117000+00:00"}' + else: + expected_json = '{"created_at":"2019-12-27T18:11:19.117000Z"}' model = Model.construct(created_at="2019-12-27T18:11:19.117Z") assert model.created_at == expected @@ -531,7 +531,7 @@ class Model2(BaseModel): assert m4.to_dict(mode="python") == {"created_at": datetime.fromisoformat(time_str)} assert m4.to_dict(mode="json") == {"created_at": time_str} - if not PYDANTIC_V2: + if PYDANTIC_V1: with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): m.to_dict(warnings=False) @@ -556,7 +556,7 @@ class Model(BaseModel): assert m3.model_dump() == {"foo": None} assert m3.model_dump(exclude_none=True) == {} - if not PYDANTIC_V2: + if PYDANTIC_V1: with pytest.raises(ValueError, match="round_trip is only supported in Pydantic v2"): m.model_dump(round_trip=True) @@ -580,10 +580,10 @@ class Model(BaseModel): assert json.loads(m.to_json()) == {"FOO": "hello"} assert json.loads(m.to_json(use_api_names=False)) == {"foo": "hello"} - if PYDANTIC_V2: - assert m.to_json(indent=None) == '{"FOO":"hello"}' - else: + if PYDANTIC_V1: assert m.to_json(indent=None) == '{"FOO": "hello"}' + else: + assert m.to_json(indent=None) == '{"FOO":"hello"}' m2 = Model() assert json.loads(m2.to_json()) == {} @@ -595,7 +595,7 @@ class Model(BaseModel): assert json.loads(m3.to_json()) == {"FOO": None} assert json.loads(m3.to_json(exclude_none=True)) == {} - if not PYDANTIC_V2: + if PYDANTIC_V1: with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): m.to_json(warnings=False) @@ -622,7 +622,7 @@ class Model(BaseModel): assert json.loads(m3.model_dump_json()) == {"foo": None} assert json.loads(m3.model_dump_json(exclude_none=True)) == {} - if not PYDANTIC_V2: + if PYDANTIC_V1: with pytest.raises(ValueError, match="round_trip is only supported in Pydantic v2"): m.model_dump_json(round_trip=True) @@ -679,12 +679,12 @@ class B(BaseModel): ) assert isinstance(m, A) assert m.type == "a" - if PYDANTIC_V2: - assert m.data == 100 # type: ignore[comparison-overlap] - else: + if PYDANTIC_V1: # pydantic v1 automatically converts inputs to strings # if the expected type is a str assert m.data == "100" + else: + assert m.data == 100 # type: ignore[comparison-overlap] def test_discriminated_unions_unknown_variant() -> None: @@ -768,12 +768,12 @@ class B(BaseModel): ) assert isinstance(m, A) assert m.foo_type == "a" - if PYDANTIC_V2: - assert m.data == 100 # type: ignore[comparison-overlap] - else: + if PYDANTIC_V1: # pydantic v1 automatically converts inputs to strings # if the expected type is a str assert m.data == "100" + else: + assert m.data == 100 # type: ignore[comparison-overlap] def test_discriminated_unions_overlapping_discriminators_invalid_data() -> None: @@ -833,7 +833,7 @@ class B(BaseModel): assert UnionType.__discriminator__ is discriminator -@pytest.mark.skipif(not PYDANTIC_V2, reason="TypeAliasType is not supported in Pydantic v1") +@pytest.mark.skipif(PYDANTIC_V1, reason="TypeAliasType is not supported in Pydantic v1") def test_type_alias_type() -> None: Alias = TypeAliasType("Alias", str) # pyright: ignore @@ -849,7 +849,7 @@ class Model(BaseModel): assert m.union == "bar" -@pytest.mark.skipif(not PYDANTIC_V2, reason="TypeAliasType is not supported in Pydantic v1") +@pytest.mark.skipif(PYDANTIC_V1, reason="TypeAliasType is not supported in Pydantic v1") def test_field_named_cls() -> None: class Model(BaseModel): cls: str @@ -936,7 +936,7 @@ class Type2(BaseModel): assert isinstance(model.value, InnerType2) -@pytest.mark.skipif(not PYDANTIC_V2, reason="this is only supported in pydantic v2 for now") +@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2 for now") def test_extra_properties() -> None: class Item(BaseModel): prop: int diff --git a/tests/test_transform.py b/tests/test_transform.py index 69233f26..fe86aaaf 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -15,7 +15,7 @@ parse_datetime, async_transform as _async_transform, ) -from writerai._compat import PYDANTIC_V2 +from writerai._compat import PYDANTIC_V1 from writerai._models import BaseModel _T = TypeVar("_T") @@ -189,7 +189,7 @@ class DateModel(BaseModel): @pytest.mark.asyncio async def test_iso8601_format(use_async: bool) -> None: dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") - tz = "Z" if PYDANTIC_V2 else "+00:00" + tz = "+00:00" if PYDANTIC_V1 else "Z" assert await transform({"foo": dt}, DatetimeDict, use_async) == {"foo": "2023-02-23T14:16:36.337692+00:00"} # type: ignore[comparison-overlap] assert await transform(DatetimeModel(foo=dt), Any, use_async) == {"foo": "2023-02-23T14:16:36.337692" + tz} # type: ignore[comparison-overlap] @@ -297,11 +297,11 @@ async def test_pydantic_unknown_field(use_async: bool) -> None: @pytest.mark.asyncio async def test_pydantic_mismatched_types(use_async: bool) -> None: model = MyModel.construct(foo=True) - if PYDANTIC_V2: + if PYDANTIC_V1: + params = await transform(model, Any, use_async) + else: with pytest.warns(UserWarning): params = await transform(model, Any, use_async) - else: - params = await transform(model, Any, use_async) assert cast(Any, params) == {"foo": True} @@ -309,11 +309,11 @@ async def test_pydantic_mismatched_types(use_async: bool) -> None: @pytest.mark.asyncio async def test_pydantic_mismatched_object_type(use_async: bool) -> None: model = MyModel.construct(foo=MyModel.construct(hello="world")) - if PYDANTIC_V2: + if PYDANTIC_V1: + params = await transform(model, Any, use_async) + else: with pytest.warns(UserWarning): params = await transform(model, Any, use_async) - else: - params = await transform(model, Any, use_async) assert cast(Any, params) == {"foo": {"hello": "world"}} diff --git a/tests/test_utils/test_datetime_parse.py b/tests/test_utils/test_datetime_parse.py new file mode 100644 index 00000000..ba09afb5 --- /dev/null +++ b/tests/test_utils/test_datetime_parse.py @@ -0,0 +1,110 @@ +""" +Copied from https://github.com/pydantic/pydantic/blob/v1.10.22/tests/test_datetime_parse.py +with modifications so it works without pydantic v1 imports. +""" + +from typing import Type, Union +from datetime import date, datetime, timezone, timedelta + +import pytest + +from writerai._utils import parse_date, parse_datetime + + +def create_tz(minutes: int) -> timezone: + return timezone(timedelta(minutes=minutes)) + + +@pytest.mark.parametrize( + "value,result", + [ + # Valid inputs + ("1494012444.883309", date(2017, 5, 5)), + (b"1494012444.883309", date(2017, 5, 5)), + (1_494_012_444.883_309, date(2017, 5, 5)), + ("1494012444", date(2017, 5, 5)), + (1_494_012_444, date(2017, 5, 5)), + (0, date(1970, 1, 1)), + ("2012-04-23", date(2012, 4, 23)), + (b"2012-04-23", date(2012, 4, 23)), + ("2012-4-9", date(2012, 4, 9)), + (date(2012, 4, 9), date(2012, 4, 9)), + (datetime(2012, 4, 9, 12, 15), date(2012, 4, 9)), + # Invalid inputs + ("x20120423", ValueError), + ("2012-04-56", ValueError), + (19_999_999_999, date(2603, 10, 11)), # just before watershed + (20_000_000_001, date(1970, 8, 20)), # just after watershed + (1_549_316_052, date(2019, 2, 4)), # nowish in s + (1_549_316_052_104, date(2019, 2, 4)), # nowish in ms + (1_549_316_052_104_324, date(2019, 2, 4)), # nowish in μs + (1_549_316_052_104_324_096, date(2019, 2, 4)), # nowish in ns + ("infinity", date(9999, 12, 31)), + ("inf", date(9999, 12, 31)), + (float("inf"), date(9999, 12, 31)), + ("infinity ", date(9999, 12, 31)), + (int("1" + "0" * 100), date(9999, 12, 31)), + (1e1000, date(9999, 12, 31)), + ("-infinity", date(1, 1, 1)), + ("-inf", date(1, 1, 1)), + ("nan", ValueError), + ], +) +def test_date_parsing(value: Union[str, bytes, int, float], result: Union[date, Type[Exception]]) -> None: + if type(result) == type and issubclass(result, Exception): # pyright: ignore[reportUnnecessaryIsInstance] + with pytest.raises(result): + parse_date(value) + else: + assert parse_date(value) == result + + +@pytest.mark.parametrize( + "value,result", + [ + # Valid inputs + # values in seconds + ("1494012444.883309", datetime(2017, 5, 5, 19, 27, 24, 883_309, tzinfo=timezone.utc)), + (1_494_012_444.883_309, datetime(2017, 5, 5, 19, 27, 24, 883_309, tzinfo=timezone.utc)), + ("1494012444", datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)), + (b"1494012444", datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)), + (1_494_012_444, datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)), + # values in ms + ("1494012444000.883309", datetime(2017, 5, 5, 19, 27, 24, 883, tzinfo=timezone.utc)), + ("-1494012444000.883309", datetime(1922, 8, 29, 4, 32, 35, 999117, tzinfo=timezone.utc)), + (1_494_012_444_000, datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)), + ("2012-04-23T09:15:00", datetime(2012, 4, 23, 9, 15)), + ("2012-4-9 4:8:16", datetime(2012, 4, 9, 4, 8, 16)), + ("2012-04-23T09:15:00Z", datetime(2012, 4, 23, 9, 15, 0, 0, timezone.utc)), + ("2012-4-9 4:8:16-0320", datetime(2012, 4, 9, 4, 8, 16, 0, create_tz(-200))), + ("2012-04-23T10:20:30.400+02:30", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(150))), + ("2012-04-23T10:20:30.400+02", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(120))), + ("2012-04-23T10:20:30.400-02", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(-120))), + (b"2012-04-23T10:20:30.400-02", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(-120))), + (datetime(2017, 5, 5), datetime(2017, 5, 5)), + (0, datetime(1970, 1, 1, 0, 0, 0, tzinfo=timezone.utc)), + # Invalid inputs + ("x20120423091500", ValueError), + ("2012-04-56T09:15:90", ValueError), + ("2012-04-23T11:05:00-25:00", ValueError), + (19_999_999_999, datetime(2603, 10, 11, 11, 33, 19, tzinfo=timezone.utc)), # just before watershed + (20_000_000_001, datetime(1970, 8, 20, 11, 33, 20, 1000, tzinfo=timezone.utc)), # just after watershed + (1_549_316_052, datetime(2019, 2, 4, 21, 34, 12, 0, tzinfo=timezone.utc)), # nowish in s + (1_549_316_052_104, datetime(2019, 2, 4, 21, 34, 12, 104_000, tzinfo=timezone.utc)), # nowish in ms + (1_549_316_052_104_324, datetime(2019, 2, 4, 21, 34, 12, 104_324, tzinfo=timezone.utc)), # nowish in μs + (1_549_316_052_104_324_096, datetime(2019, 2, 4, 21, 34, 12, 104_324, tzinfo=timezone.utc)), # nowish in ns + ("infinity", datetime(9999, 12, 31, 23, 59, 59, 999999)), + ("inf", datetime(9999, 12, 31, 23, 59, 59, 999999)), + ("inf ", datetime(9999, 12, 31, 23, 59, 59, 999999)), + (1e50, datetime(9999, 12, 31, 23, 59, 59, 999999)), + (float("inf"), datetime(9999, 12, 31, 23, 59, 59, 999999)), + ("-infinity", datetime(1, 1, 1, 0, 0)), + ("-inf", datetime(1, 1, 1, 0, 0)), + ("nan", ValueError), + ], +) +def test_datetime_parsing(value: Union[str, bytes, int, float], result: Union[datetime, Type[Exception]]) -> None: + if type(result) == type and issubclass(result, Exception): # pyright: ignore[reportUnnecessaryIsInstance] + with pytest.raises(result): + parse_datetime(value) + else: + assert parse_datetime(value) == result diff --git a/tests/utils.py b/tests/utils.py index c944be62..7e3ba26a 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -19,7 +19,7 @@ is_annotated_type, is_type_alias_type, ) -from writerai._compat import PYDANTIC_V2, field_outer_type, get_model_fields +from writerai._compat import PYDANTIC_V1, field_outer_type, get_model_fields from writerai._models import BaseModel BaseModelT = TypeVar("BaseModelT", bound=BaseModel) @@ -28,12 +28,12 @@ def assert_matches_model(model: type[BaseModelT], value: BaseModelT, *, path: list[str]) -> bool: for name, field in get_model_fields(model).items(): field_value = getattr(value, name) - if PYDANTIC_V2: - allow_none = False - else: + if PYDANTIC_V1: # in v1 nullability was structured differently # https://docs.pydantic.dev/2.0/migration/#required-optional-and-nullable-fields allow_none = getattr(field, "allow_none", False) + else: + allow_none = False assert_matches_type( field_outer_type(field), From a1e0264020443b4cc1ae610de5a74f5ccf872b25 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 14:48:27 +0000 Subject: [PATCH 308/399] chore(internal): move mypy configurations to `pyproject.toml` file --- mypy.ini | 50 ------------------------------------------------ pyproject.toml | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 50 deletions(-) delete mode 100644 mypy.ini diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index 4a26acff..00000000 --- a/mypy.ini +++ /dev/null @@ -1,50 +0,0 @@ -[mypy] -pretty = True -show_error_codes = True - -# Exclude _files.py because mypy isn't smart enough to apply -# the correct type narrowing and as this is an internal module -# it's fine to just use Pyright. -# -# We also exclude our `tests` as mypy doesn't always infer -# types correctly and Pyright will still catch any type errors. -exclude = ^(src/writerai/_files\.py|_dev/.*\.py|tests/.*)$ - -strict_equality = True -implicit_reexport = True -check_untyped_defs = True -no_implicit_optional = True - -warn_return_any = True -warn_unreachable = True -warn_unused_configs = True - -# Turn these options off as it could cause conflicts -# with the Pyright options. -warn_unused_ignores = False -warn_redundant_casts = False - -disallow_any_generics = True -disallow_untyped_defs = True -disallow_untyped_calls = True -disallow_subclassing_any = True -disallow_incomplete_defs = True -disallow_untyped_decorators = True -cache_fine_grained = True - -# By default, mypy reports an error if you assign a value to the result -# of a function call that doesn't return anything. We do this in our test -# cases: -# ``` -# result = ... -# assert result is None -# ``` -# Changing this codegen to make mypy happy would increase complexity -# and would not be worth it. -disable_error_code = func-returns-value,overload-cannot-match - -# https://github.com/python/mypy/issues/12162 -[mypy.overrides] -module = "black.files.*" -ignore_errors = true -ignore_missing_imports = true diff --git a/pyproject.toml b/pyproject.toml index c8546583..838bee99 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -157,6 +157,58 @@ reportOverlappingOverload = false reportImportCycles = false reportPrivateUsage = false +[tool.mypy] +pretty = true +show_error_codes = true + +# Exclude _files.py because mypy isn't smart enough to apply +# the correct type narrowing and as this is an internal module +# it's fine to just use Pyright. +# +# We also exclude our `tests` as mypy doesn't always infer +# types correctly and Pyright will still catch any type errors. +exclude = ['src/writerai/_files.py', '_dev/.*.py', 'tests/.*'] + +strict_equality = true +implicit_reexport = true +check_untyped_defs = true +no_implicit_optional = true + +warn_return_any = true +warn_unreachable = true +warn_unused_configs = true + +# Turn these options off as it could cause conflicts +# with the Pyright options. +warn_unused_ignores = false +warn_redundant_casts = false + +disallow_any_generics = true +disallow_untyped_defs = true +disallow_untyped_calls = true +disallow_subclassing_any = true +disallow_incomplete_defs = true +disallow_untyped_decorators = true +cache_fine_grained = true + +# By default, mypy reports an error if you assign a value to the result +# of a function call that doesn't return anything. We do this in our test +# cases: +# ``` +# result = ... +# assert result is None +# ``` +# Changing this codegen to make mypy happy would increase complexity +# and would not be worth it. +disable_error_code = "func-returns-value,overload-cannot-match" + +# https://github.com/python/mypy/issues/12162 +[[tool.mypy.overrides]] +module = "black.files.*" +ignore_errors = true +ignore_missing_imports = true + + [tool.ruff] line-length = 120 output-format = "grouped" From 40fe7c1736cd7829aa5251de292bc583fb3f94c7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 22:00:18 +0000 Subject: [PATCH 309/399] docs(api): updates to API spec --- .stats.yml | 4 ++-- src/writerai/types/application_list_response.py | 3 +++ src/writerai/types/application_retrieve_response.py | 3 +++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index cf180159..9ef1b2b5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 33 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-9e04c6a51c55704029c529f2917d0e2b976cb7b6595128697db031ad7bd61a63.yml -openapi_spec_hash: e8a95522dd13ffe4633cccc34bbd651d +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-321826aa393f6f2c2e2ccc8b20795157f8b55908a96b4c28d90272f306ee3ff4.yml +openapi_spec_hash: ccf23a9557962bab6ed52a94a25ecf1c config_hash: 7a38bab086b53b43d2a719cb4d883264 diff --git a/src/writerai/types/application_list_response.py b/src/writerai/types/application_list_response.py index 7f8e9fb0..e80b6e90 100644 --- a/src/writerai/types/application_list_response.py +++ b/src/writerai/types/application_list_response.py @@ -35,6 +35,9 @@ class InputOptionsApplicationInputFileOptions(BaseModel): max_word_count: int """Maximum number of words allowed in text files.""" + upload_types: List[Literal["url", "file_id"]] + """List of allowed upload types for file inputs.""" + class InputOptionsApplicationInputMediaOptions(BaseModel): file_types: List[str] diff --git a/src/writerai/types/application_retrieve_response.py b/src/writerai/types/application_retrieve_response.py index c195b145..00827b7a 100644 --- a/src/writerai/types/application_retrieve_response.py +++ b/src/writerai/types/application_retrieve_response.py @@ -35,6 +35,9 @@ class InputOptionsApplicationInputFileOptions(BaseModel): max_word_count: int """Maximum number of words allowed in text files.""" + upload_types: List[Literal["url", "file_id"]] + """List of allowed upload types for file inputs.""" + class InputOptionsApplicationInputMediaOptions(BaseModel): file_types: List[str] From 4a84cfd5f383f8592ec1457c8bbdaa83f7780c5b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 21:26:04 +0000 Subject: [PATCH 310/399] chore(tests): simplify `get_platform` test `nest_asyncio` is archived and broken on some platforms so it's not worth keeping in our test suite. --- pyproject.toml | 1 - requirements-dev.lock | 1 - tests/test_client.py | 53 +++++-------------------------------------- 3 files changed, 6 insertions(+), 49 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 838bee99..598b9c21 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,6 @@ dev-dependencies = [ "dirty-equals>=0.6.0", "importlib-metadata>=6.7.0", "rich>=13.7.1", - "nest_asyncio==1.6.0", "pytest-xdist>=3.6.1", ] diff --git a/requirements-dev.lock b/requirements-dev.lock index 6b34ee8e..57f99645 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -75,7 +75,6 @@ multidict==6.4.4 mypy==1.14.1 mypy-extensions==1.0.0 # via mypy -nest-asyncio==1.6.0 nodeenv==1.8.0 # via pyright nox==2023.4.22 diff --git a/tests/test_client.py b/tests/test_client.py index 03dbad51..c8a764ce 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -6,13 +6,10 @@ import os import sys import json -import time import asyncio import inspect -import subprocess import tracemalloc from typing import Any, Union, cast -from textwrap import dedent from unittest import mock from typing_extensions import Literal @@ -23,6 +20,7 @@ from writerai import Writer, AsyncWriter, APIResponseValidationError from writerai._types import Omit +from writerai._utils import asyncify from writerai._models import BaseModel, FinalRequestOptions from writerai._streaming import Stream, AsyncStream from writerai._exceptions import WriterError, APIStatusError, APITimeoutError, APIResponseValidationError @@ -30,8 +28,10 @@ DEFAULT_TIMEOUT, HTTPX_DEFAULT_TIMEOUT, BaseClient, + OtherPlatform, DefaultHttpxClient, DefaultAsyncHttpxClient, + get_platform, make_request_options, ) @@ -1659,50 +1659,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: assert response.http_request.headers.get("x-stainless-retry-count") == "42" - def test_get_platform(self) -> None: - # A previous implementation of asyncify could leave threads unterminated when - # used with nest_asyncio. - # - # Since nest_asyncio.apply() is global and cannot be un-applied, this - # test is run in a separate process to avoid affecting other tests. - test_code = dedent(""" - import asyncio - import nest_asyncio - import threading - - from writerai._utils import asyncify - from writerai._base_client import get_platform - - async def test_main() -> None: - result = await asyncify(get_platform)() - print(result) - for thread in threading.enumerate(): - print(thread.name) - - nest_asyncio.apply() - asyncio.run(test_main()) - """) - with subprocess.Popen( - [sys.executable, "-c", test_code], - text=True, - ) as process: - timeout = 10 # seconds - - start_time = time.monotonic() - while True: - return_code = process.poll() - if return_code is not None: - if return_code != 0: - raise AssertionError("calling get_platform using asyncify resulted in a non-zero exit code") - - # success - break - - if time.monotonic() - start_time > timeout: - process.kill() - raise AssertionError("calling get_platform using asyncify resulted in a hung process") - - time.sleep(0.1) + async def test_get_platform(self) -> None: + platform = await asyncify(get_platform)() + assert isinstance(platform, (str, OtherPlatform)) async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: # Test that the proxy environment variables are set correctly From 05ea6c4b9e01f2b429be16538ba649d67560554c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 10 Sep 2025 18:47:14 +0000 Subject: [PATCH 311/399] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 9ef1b2b5..ca6b61b9 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 33 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-321826aa393f6f2c2e2ccc8b20795157f8b55908a96b4c28d90272f306ee3ff4.yml openapi_spec_hash: ccf23a9557962bab6ed52a94a25ecf1c -config_hash: 7a38bab086b53b43d2a719cb4d883264 +config_hash: d655a846f6872554a75412b27b6ed71f From 92a6a2028cb423979d95fd8fa96d76acb2b1bdb3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 20:01:01 +0000 Subject: [PATCH 312/399] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index ca6b61b9..9ef1b2b5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 33 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-321826aa393f6f2c2e2ccc8b20795157f8b55908a96b4c28d90272f306ee3ff4.yml openapi_spec_hash: ccf23a9557962bab6ed52a94a25ecf1c -config_hash: d655a846f6872554a75412b27b6ed71f +config_hash: 7a38bab086b53b43d2a719cb4d883264 From 2d1aff987fa5ad4ebc0fb4e712d9e7c9980226da Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 20:35:18 +0000 Subject: [PATCH 313/399] docs(api): updates to API spec --- .stats.yml | 4 +- src/writerai/resources/graphs.py | 28 +++++++ src/writerai/types/graph_question_params.py | 75 ++++++++++++++++++- src/writerai/types/question.py | 69 ++++++++++++++++- src/writerai/types/shared/graph_data.py | 5 +- src/writerai/types/shared/source.py | 7 +- src/writerai/types/shared/tool_param.py | 74 ++++++++++++++++++ .../types/shared_params/graph_data.py | 5 +- src/writerai/types/shared_params/source.py | 7 +- .../types/shared_params/tool_param.py | 74 ++++++++++++++++++ tests/api_resources/test_graphs.py | 40 ++++++++++ 11 files changed, 374 insertions(+), 14 deletions(-) diff --git a/.stats.yml b/.stats.yml index 9ef1b2b5..1822dc87 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 33 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-321826aa393f6f2c2e2ccc8b20795157f8b55908a96b4c28d90272f306ee3ff4.yml -openapi_spec_hash: ccf23a9557962bab6ed52a94a25ecf1c +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-3f87c8deb39e443022f2e04252994a6c9d25473872503edf9eec00d874576b2d.yml +openapi_spec_hash: 5de52bf1d78e00b13a04f6e9ce2f2fb5 config_hash: 7a38bab086b53b43d2a719cb4d883264 diff --git a/src/writerai/resources/graphs.py b/src/writerai/resources/graphs.py index e3fe5cac..69ad92ed 100644 --- a/src/writerai/resources/graphs.py +++ b/src/writerai/resources/graphs.py @@ -326,6 +326,7 @@ def question( *, graph_ids: SequenceNotStr[str], question: str, + query_config: graph_question_params.QueryConfig | NotGiven = NOT_GIVEN, stream: Literal[False] | NotGiven = NOT_GIVEN, subqueries: bool | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -343,6 +344,9 @@ def question( question: The question to answer using the Knowledge Graph. + query_config: Configuration options for Knowledge Graph queries, including search parameters + and citation settings. + stream: Determines whether the model's output should be streamed. If true, the output is generated and sent incrementally, which can be useful for real-time applications. @@ -366,6 +370,7 @@ def question( graph_ids: SequenceNotStr[str], question: str, stream: Literal[True], + query_config: graph_question_params.QueryConfig | NotGiven = NOT_GIVEN, subqueries: bool | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -386,6 +391,9 @@ def question( generated and sent incrementally, which can be useful for real-time applications. + query_config: Configuration options for Knowledge Graph queries, including search parameters + and citation settings. + subqueries: Specify whether to include subqueries. extra_headers: Send extra headers @@ -405,6 +413,7 @@ def question( graph_ids: SequenceNotStr[str], question: str, stream: bool, + query_config: graph_question_params.QueryConfig | NotGiven = NOT_GIVEN, subqueries: bool | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -425,6 +434,9 @@ def question( generated and sent incrementally, which can be useful for real-time applications. + query_config: Configuration options for Knowledge Graph queries, including search parameters + and citation settings. + subqueries: Specify whether to include subqueries. extra_headers: Send extra headers @@ -443,6 +455,7 @@ def question( *, graph_ids: SequenceNotStr[str], question: str, + query_config: graph_question_params.QueryConfig | NotGiven = NOT_GIVEN, stream: Literal[False] | Literal[True] | NotGiven = NOT_GIVEN, subqueries: bool | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -458,6 +471,7 @@ def question( { "graph_ids": graph_ids, "question": question, + "query_config": query_config, "stream": stream, "subqueries": subqueries, }, @@ -799,6 +813,7 @@ async def question( *, graph_ids: SequenceNotStr[str], question: str, + query_config: graph_question_params.QueryConfig | NotGiven = NOT_GIVEN, stream: Literal[False] | NotGiven = NOT_GIVEN, subqueries: bool | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -816,6 +831,9 @@ async def question( question: The question to answer using the Knowledge Graph. + query_config: Configuration options for Knowledge Graph queries, including search parameters + and citation settings. + stream: Determines whether the model's output should be streamed. If true, the output is generated and sent incrementally, which can be useful for real-time applications. @@ -839,6 +857,7 @@ async def question( graph_ids: SequenceNotStr[str], question: str, stream: Literal[True], + query_config: graph_question_params.QueryConfig | NotGiven = NOT_GIVEN, subqueries: bool | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -859,6 +878,9 @@ async def question( generated and sent incrementally, which can be useful for real-time applications. + query_config: Configuration options for Knowledge Graph queries, including search parameters + and citation settings. + subqueries: Specify whether to include subqueries. extra_headers: Send extra headers @@ -878,6 +900,7 @@ async def question( graph_ids: SequenceNotStr[str], question: str, stream: bool, + query_config: graph_question_params.QueryConfig | NotGiven = NOT_GIVEN, subqueries: bool | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -898,6 +921,9 @@ async def question( generated and sent incrementally, which can be useful for real-time applications. + query_config: Configuration options for Knowledge Graph queries, including search parameters + and citation settings. + subqueries: Specify whether to include subqueries. extra_headers: Send extra headers @@ -916,6 +942,7 @@ async def question( *, graph_ids: SequenceNotStr[str], question: str, + query_config: graph_question_params.QueryConfig | NotGiven = NOT_GIVEN, stream: Literal[False] | Literal[True] | NotGiven = NOT_GIVEN, subqueries: bool | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -931,6 +958,7 @@ async def question( { "graph_ids": graph_ids, "question": question, + "query_config": query_config, "stream": stream, "subqueries": subqueries, }, diff --git a/src/writerai/types/graph_question_params.py b/src/writerai/types/graph_question_params.py index 02e8b513..a49563c7 100644 --- a/src/writerai/types/graph_question_params.py +++ b/src/writerai/types/graph_question_params.py @@ -7,7 +7,7 @@ from .._types import SequenceNotStr -__all__ = ["GraphQuestionParamsBase", "GraphQuestionParamsNonStreaming", "GraphQuestionParamsStreaming"] +__all__ = ["GraphQuestionParamsBase", "QueryConfig", "GraphQuestionParamsNonStreaming", "GraphQuestionParamsStreaming"] class GraphQuestionParamsBase(TypedDict, total=False): @@ -17,10 +17,83 @@ class GraphQuestionParamsBase(TypedDict, total=False): question: Required[str] """The question to answer using the Knowledge Graph.""" + query_config: QueryConfig + """ + Configuration options for Knowledge Graph queries, including search parameters + and citation settings. + """ + subqueries: bool """Specify whether to include subqueries.""" +class QueryConfig(TypedDict, total=False): + grounding_level: float + """ + Level of grounding required for responses, controlling how closely answers must + be tied to source material. Set lower for grounded outputs, higher for + creativity. Higher values (closer to 1.0) allow more creative interpretation, + while lower values (closer to 0.0) stick more closely to source material. Range: + 0.0-1.0, Default: 0.0. + """ + + inline_citations: bool + """ + Whether to include inline citations in the response, showing which Knowledge + Graph sources were used. Default: false. + """ + + keyword_threshold: float + """Threshold for keyword-based matching when searching Knowledge Graph content. + + Set higher for stricter relevance, lower for broader range. Higher values + (closer to 1.0) require stronger keyword matches, while lower values (closer to + 0.0) allow more lenient matching. Range: 0.0-1.0, Default: 0.7. + """ + + max_snippets: int + """Maximum number of text snippets to retrieve from the Knowledge Graph for + context. + + Works in concert with `search_weight` to control best matches vs broader + coverage. While technically supports 1-60, values below 5 may return no results + due to RAG implementation. Recommended range: 5-25. Due to RAG system behavior, + you may see more snippets than requested. Range: 1-60, Default: 30. + """ + + max_subquestions: int + """Maximum number of subquestions to generate when processing complex queries. + + Set higher to improve detail, set lower to reduce response time. Range: 1-10, + Default: 6. + """ + + max_tokens: int + """Maximum number of tokens the model can generate in the response. + + This controls the length of the AI's answer. Set higher for longer answers, set + lower for shorter, faster answers. Range: 100-8000, Default: 4000. + """ + + search_weight: int + """Weight given to search results when ranking and selecting relevant information. + + Higher values (closer to 100) prioritize keyword-based matching, while lower + values (closer to 0) prioritize semantic similarity matching. Use higher values + for exact keyword searches, lower values for conceptual similarity searches. + Range: 0-100, Default: 50. + """ + + semantic_threshold: float + """ + Threshold for semantic similarity matching when searching Knowledge Graph + content. Set higher for stricter relevance, lower for broader range. Higher + values (closer to 1.0) require stronger semantic similarity, while lower values + (closer to 0.0) allow more lenient semantic matching. Range: 0.0-1.0, Default: + 0.7. + """ + + class GraphQuestionParamsNonStreaming(GraphQuestionParamsBase, total=False): stream: Literal[False] """Determines whether the model's output should be streamed. diff --git a/src/writerai/types/question.py b/src/writerai/types/question.py index 4d4d8871..9f3095bb 100644 --- a/src/writerai/types/question.py +++ b/src/writerai/types/question.py @@ -2,20 +2,77 @@ from typing import List, Optional +from pydantic import Field as FieldInfo + from .._models import BaseModel from .shared.source import Source -__all__ = ["Question", "Subquery"] +__all__ = ["Question", "References", "ReferencesFile", "ReferencesWeb", "Subquery"] + + +class ReferencesFile(BaseModel): + file_id: str = FieldInfo(alias="fileId") + """The unique identifier of the file in your Writer account.""" + + score: float + """ + Internal score used during the retrieval process for ranking and selecting + relevant snippets. + """ + + text: str + """ + The exact text snippet from the source document that was used to support the + response. + """ + + cite: Optional[str] = None + """ + Unique citation ID that appears in inline citations within the response text + (null if not cited). + """ + + page: Optional[int] = None + """Page number where this snippet was found in the source document.""" + + +class ReferencesWeb(BaseModel): + score: float + """ + Internal score used during the retrieval process for ranking and selecting + relevant snippets. + """ + + text: str + """ + The exact text snippet from the web source that was used to support the + response. + """ + + title: str + """The title of the web page where this content was found.""" + + url: str + """The URL of the web page where this content was found.""" + + +class References(BaseModel): + files: Optional[List[ReferencesFile]] = None + """Array of file-based references from uploaded documents in the Knowledge Graph.""" + + web: Optional[List[ReferencesWeb]] = None + """Array of web-based references from online sources accessed during the query.""" class Subquery(BaseModel): answer: str - """The answer to the subquery.""" + """The answer to the subquery based on Knowledge Graph content.""" query: str - """The subquery that was asked.""" + """The subquery that was generated to help answer the main question.""" sources: List[Optional[Source]] + """Array of source snippets that were used to answer this subquery.""" class Question(BaseModel): @@ -27,4 +84,10 @@ class Question(BaseModel): sources: List[Optional[Source]] + references: Optional[References] = None + """ + Detailed source information organized by reference type, providing comprehensive + metadata about the sources used to generate the response. + """ + subqueries: Optional[List[Optional[Subquery]]] = None diff --git a/src/writerai/types/shared/graph_data.py b/src/writerai/types/shared/graph_data.py index ad78cf72..6898bddd 100644 --- a/src/writerai/types/shared/graph_data.py +++ b/src/writerai/types/shared/graph_data.py @@ -11,12 +11,13 @@ class Subquery(BaseModel): answer: str - """The answer to the subquery.""" + """The answer to the subquery based on Knowledge Graph content.""" query: str - """The subquery that was asked.""" + """The subquery that was generated to help answer the main question.""" sources: List[Optional[Source]] + """Array of source snippets that were used to answer this subquery.""" class GraphData(BaseModel): diff --git a/src/writerai/types/shared/source.py b/src/writerai/types/shared/source.py index 36ae2008..326d01ad 100644 --- a/src/writerai/types/shared/source.py +++ b/src/writerai/types/shared/source.py @@ -7,7 +7,10 @@ class Source(BaseModel): file_id: str - """The unique identifier of the file.""" + """The unique identifier of the file in your Writer account.""" snippet: str - """A snippet of text from the source file.""" + """ + The exact text snippet from the source document that was used to support the + response. + """ diff --git a/src/writerai/types/shared/tool_param.py b/src/writerai/types/shared/tool_param.py index f5186ef1..c88d8aec 100644 --- a/src/writerai/types/shared/tool_param.py +++ b/src/writerai/types/shared/tool_param.py @@ -12,6 +12,7 @@ "FunctionTool", "GraphTool", "GraphToolFunction", + "GraphToolFunctionQueryConfig", "LlmTool", "LlmToolFunction", "TranslationTool", @@ -32,6 +33,73 @@ class FunctionTool(BaseModel): """The type of tool.""" +class GraphToolFunctionQueryConfig(BaseModel): + grounding_level: Optional[float] = None + """ + Level of grounding required for responses, controlling how closely answers must + be tied to source material. Set lower for grounded outputs, higher for + creativity. Higher values (closer to 1.0) allow more creative interpretation, + while lower values (closer to 0.0) stick more closely to source material. Range: + 0.0-1.0, Default: 0.0. + """ + + inline_citations: Optional[bool] = None + """ + Whether to include inline citations in the response, showing which Knowledge + Graph sources were used. Default: false. + """ + + keyword_threshold: Optional[float] = None + """Threshold for keyword-based matching when searching Knowledge Graph content. + + Set higher for stricter relevance, lower for broader range. Higher values + (closer to 1.0) require stronger keyword matches, while lower values (closer to + 0.0) allow more lenient matching. Range: 0.0-1.0, Default: 0.7. + """ + + max_snippets: Optional[int] = None + """Maximum number of text snippets to retrieve from the Knowledge Graph for + context. + + Works in concert with `search_weight` to control best matches vs broader + coverage. While technically supports 1-60, values below 5 may return no results + due to RAG implementation. Recommended range: 5-25. Due to RAG system behavior, + you may see more snippets than requested. Range: 1-60, Default: 30. + """ + + max_subquestions: Optional[int] = None + """Maximum number of subquestions to generate when processing complex queries. + + Set higher to improve detail, set lower to reduce response time. Range: 1-10, + Default: 6. + """ + + max_tokens: Optional[int] = None + """Maximum number of tokens the model can generate in the response. + + This controls the length of the AI's answer. Set higher for longer answers, set + lower for shorter, faster answers. Range: 100-8000, Default: 4000. + """ + + search_weight: Optional[int] = None + """Weight given to search results when ranking and selecting relevant information. + + Higher values (closer to 100) prioritize keyword-based matching, while lower + values (closer to 0) prioritize semantic similarity matching. Use higher values + for exact keyword searches, lower values for conceptual similarity searches. + Range: 0-100, Default: 50. + """ + + semantic_threshold: Optional[float] = None + """ + Threshold for semantic similarity matching when searching Knowledge Graph + content. Set higher for stricter relevance, lower for broader range. Higher + values (closer to 1.0) require stronger semantic similarity, while lower values + (closer to 0.0) allow more lenient semantic matching. Range: 0.0-1.0, Default: + 0.7. + """ + + class GraphToolFunction(BaseModel): graph_ids: List[str] """An array of graph IDs to use in the tool.""" @@ -42,6 +110,12 @@ class GraphToolFunction(BaseModel): description: Optional[str] = None """A description of the graph content.""" + query_config: Optional[GraphToolFunctionQueryConfig] = None + """ + Configuration options for Knowledge Graph queries, including search parameters + and citation settings. + """ + class GraphTool(BaseModel): function: GraphToolFunction diff --git a/src/writerai/types/shared_params/graph_data.py b/src/writerai/types/shared_params/graph_data.py index 39ede602..caa1e6d5 100644 --- a/src/writerai/types/shared_params/graph_data.py +++ b/src/writerai/types/shared_params/graph_data.py @@ -12,12 +12,13 @@ class Subquery(TypedDict, total=False): answer: Required[str] - """The answer to the subquery.""" + """The answer to the subquery based on Knowledge Graph content.""" query: Required[str] - """The subquery that was asked.""" + """The subquery that was generated to help answer the main question.""" sources: Required[Iterable[Optional[Source]]] + """Array of source snippets that were used to answer this subquery.""" class GraphData(TypedDict, total=False): diff --git a/src/writerai/types/shared_params/source.py b/src/writerai/types/shared_params/source.py index 54b70059..dc6726ba 100644 --- a/src/writerai/types/shared_params/source.py +++ b/src/writerai/types/shared_params/source.py @@ -9,7 +9,10 @@ class Source(TypedDict, total=False): file_id: Required[str] - """The unique identifier of the file.""" + """The unique identifier of the file in your Writer account.""" snippet: Required[str] - """A snippet of text from the source file.""" + """ + The exact text snippet from the source document that was used to support the + response. + """ diff --git a/src/writerai/types/shared_params/tool_param.py b/src/writerai/types/shared_params/tool_param.py index 677a33bd..c881bcb5 100644 --- a/src/writerai/types/shared_params/tool_param.py +++ b/src/writerai/types/shared_params/tool_param.py @@ -13,6 +13,7 @@ "FunctionTool", "GraphTool", "GraphToolFunction", + "GraphToolFunctionQueryConfig", "LlmTool", "LlmToolFunction", "TranslationTool", @@ -33,6 +34,73 @@ class FunctionTool(TypedDict, total=False): """The type of tool.""" +class GraphToolFunctionQueryConfig(TypedDict, total=False): + grounding_level: float + """ + Level of grounding required for responses, controlling how closely answers must + be tied to source material. Set lower for grounded outputs, higher for + creativity. Higher values (closer to 1.0) allow more creative interpretation, + while lower values (closer to 0.0) stick more closely to source material. Range: + 0.0-1.0, Default: 0.0. + """ + + inline_citations: bool + """ + Whether to include inline citations in the response, showing which Knowledge + Graph sources were used. Default: false. + """ + + keyword_threshold: float + """Threshold for keyword-based matching when searching Knowledge Graph content. + + Set higher for stricter relevance, lower for broader range. Higher values + (closer to 1.0) require stronger keyword matches, while lower values (closer to + 0.0) allow more lenient matching. Range: 0.0-1.0, Default: 0.7. + """ + + max_snippets: int + """Maximum number of text snippets to retrieve from the Knowledge Graph for + context. + + Works in concert with `search_weight` to control best matches vs broader + coverage. While technically supports 1-60, values below 5 may return no results + due to RAG implementation. Recommended range: 5-25. Due to RAG system behavior, + you may see more snippets than requested. Range: 1-60, Default: 30. + """ + + max_subquestions: int + """Maximum number of subquestions to generate when processing complex queries. + + Set higher to improve detail, set lower to reduce response time. Range: 1-10, + Default: 6. + """ + + max_tokens: int + """Maximum number of tokens the model can generate in the response. + + This controls the length of the AI's answer. Set higher for longer answers, set + lower for shorter, faster answers. Range: 100-8000, Default: 4000. + """ + + search_weight: int + """Weight given to search results when ranking and selecting relevant information. + + Higher values (closer to 100) prioritize keyword-based matching, while lower + values (closer to 0) prioritize semantic similarity matching. Use higher values + for exact keyword searches, lower values for conceptual similarity searches. + Range: 0-100, Default: 50. + """ + + semantic_threshold: float + """ + Threshold for semantic similarity matching when searching Knowledge Graph + content. Set higher for stricter relevance, lower for broader range. Higher + values (closer to 1.0) require stronger semantic similarity, while lower values + (closer to 0.0) allow more lenient semantic matching. Range: 0.0-1.0, Default: + 0.7. + """ + + class GraphToolFunction(TypedDict, total=False): graph_ids: Required[SequenceNotStr[str]] """An array of graph IDs to use in the tool.""" @@ -43,6 +111,12 @@ class GraphToolFunction(TypedDict, total=False): description: str """A description of the graph content.""" + query_config: GraphToolFunctionQueryConfig + """ + Configuration options for Knowledge Graph queries, including search parameters + and citation settings. + """ + class GraphTool(TypedDict, total=False): function: Required[GraphToolFunction] diff --git a/tests/api_resources/test_graphs.py b/tests/api_resources/test_graphs.py index 005c9cab..d4859225 100644 --- a/tests/api_resources/test_graphs.py +++ b/tests/api_resources/test_graphs.py @@ -279,6 +279,16 @@ def test_method_question_with_all_params_overload_1(self, client: Writer) -> Non graph = client.graphs.question( graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], question="question", + query_config={ + "grounding_level": 0, + "inline_citations": True, + "keyword_threshold": 0, + "max_snippets": 1, + "max_subquestions": 1, + "max_tokens": 100, + "search_weight": 0, + "semantic_threshold": 0, + }, stream=False, subqueries=True, ) @@ -325,6 +335,16 @@ def test_method_question_with_all_params_overload_2(self, client: Writer) -> Non graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], question="question", stream=True, + query_config={ + "grounding_level": 0, + "inline_citations": True, + "keyword_threshold": 0, + "max_snippets": 1, + "max_subquestions": 1, + "max_tokens": 100, + "search_weight": 0, + "semantic_threshold": 0, + }, subqueries=True, ) graph_stream.response.close() @@ -663,6 +683,16 @@ async def test_method_question_with_all_params_overload_1(self, async_client: As graph = await async_client.graphs.question( graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], question="question", + query_config={ + "grounding_level": 0, + "inline_citations": True, + "keyword_threshold": 0, + "max_snippets": 1, + "max_subquestions": 1, + "max_tokens": 100, + "search_weight": 0, + "semantic_threshold": 0, + }, stream=False, subqueries=True, ) @@ -709,6 +739,16 @@ async def test_method_question_with_all_params_overload_2(self, async_client: As graph_ids=["182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"], question="question", stream=True, + query_config={ + "grounding_level": 0, + "inline_citations": True, + "keyword_threshold": 0, + "max_snippets": 1, + "max_subquestions": 1, + "max_tokens": 100, + "search_weight": 0, + "semantic_threshold": 0, + }, subqueries=True, ) await graph_stream.response.aclose() From 6eddda9efbacbf470ec1d0f6116e5509322792a2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 22:24:51 +0000 Subject: [PATCH 314/399] chore(internal): version bump --- .release-please-manifest.json | 2 +- README.md | 4 ++-- pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 2d96c4d3..f1d45d57 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.3.1" + ".": "2.3.2-rc1" } \ No newline at end of file diff --git a/README.md b/README.md index 3ecfe7d9..8e77ddea 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ The REST API documentation can be found on [dev.writer.com](https://dev.writer.c ```sh # install from PyPI -pip install writer-sdk +pip install --pre writer-sdk ``` ## Usage @@ -89,7 +89,7 @@ You can enable this by installing `aiohttp`: ```sh # install from PyPI -pip install writer-sdk[aiohttp] +pip install --pre writer-sdk[aiohttp] ``` Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: diff --git a/pyproject.toml b/pyproject.toml index 598b9c21..a1d4f83d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "2.3.1" +version = "2.3.2-rc1" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index fdf45740..8269c8fb 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "2.3.1" # x-release-please-version +__version__ = "2.3.2-rc1" # x-release-please-version From 57cbb48584d10b4fae2041dffb280259e1478d63 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 16 Sep 2025 19:12:38 +0000 Subject: [PATCH 315/399] chore(internal): update pydantic dependency --- requirements-dev.lock | 7 +++++-- requirements.lock | 7 +++++-- src/writerai/_models.py | 14 ++++++++++---- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/requirements-dev.lock b/requirements-dev.lock index 57f99645..f024e4cb 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -88,9 +88,9 @@ pluggy==1.5.0 propcache==0.3.1 # via aiohttp # via yarl -pydantic==2.10.3 +pydantic==2.11.9 # via writer-sdk -pydantic-core==2.27.1 +pydantic-core==2.33.2 # via pydantic pygments==2.18.0 # via rich @@ -125,7 +125,10 @@ typing-extensions==4.12.2 # via pydantic # via pydantic-core # via pyright + # via typing-inspection # via writer-sdk +typing-inspection==0.4.1 + # via pydantic virtualenv==20.24.5 # via nox yarl==1.20.0 diff --git a/requirements.lock b/requirements.lock index 65ee24ec..a26626d9 100644 --- a/requirements.lock +++ b/requirements.lock @@ -55,9 +55,9 @@ multidict==6.4.4 propcache==0.3.1 # via aiohttp # via yarl -pydantic==2.10.3 +pydantic==2.11.9 # via writer-sdk -pydantic-core==2.27.1 +pydantic-core==2.33.2 # via pydantic sniffio==1.3.0 # via anyio @@ -67,6 +67,9 @@ typing-extensions==4.12.2 # via multidict # via pydantic # via pydantic-core + # via typing-inspection # via writer-sdk +typing-inspection==0.4.1 + # via pydantic yarl==1.20.0 # via aiohttp diff --git a/src/writerai/_models.py b/src/writerai/_models.py index 3a6017ef..6a3cd1d2 100644 --- a/src/writerai/_models.py +++ b/src/writerai/_models.py @@ -256,7 +256,7 @@ def model_dump( mode: Literal["json", "python"] | str = "python", include: IncEx | None = None, exclude: IncEx | None = None, - by_alias: bool = False, + by_alias: bool | None = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, @@ -264,6 +264,7 @@ def model_dump( warnings: bool | Literal["none", "warn", "error"] = True, context: dict[str, Any] | None = None, serialize_as_any: bool = False, + fallback: Callable[[Any], Any] | None = None, ) -> dict[str, Any]: """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump @@ -295,10 +296,12 @@ def model_dump( raise ValueError("context is only supported in Pydantic v2") if serialize_as_any != False: raise ValueError("serialize_as_any is only supported in Pydantic v2") + if fallback is not None: + raise ValueError("fallback is only supported in Pydantic v2") dumped = super().dict( # pyright: ignore[reportDeprecated] include=include, exclude=exclude, - by_alias=by_alias, + by_alias=by_alias if by_alias is not None else False, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, exclude_none=exclude_none, @@ -313,13 +316,14 @@ def model_dump_json( indent: int | None = None, include: IncEx | None = None, exclude: IncEx | None = None, - by_alias: bool = False, + by_alias: bool | None = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, round_trip: bool = False, warnings: bool | Literal["none", "warn", "error"] = True, context: dict[str, Any] | None = None, + fallback: Callable[[Any], Any] | None = None, serialize_as_any: bool = False, ) -> str: """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump_json @@ -348,11 +352,13 @@ def model_dump_json( raise ValueError("context is only supported in Pydantic v2") if serialize_as_any != False: raise ValueError("serialize_as_any is only supported in Pydantic v2") + if fallback is not None: + raise ValueError("fallback is only supported in Pydantic v2") return super().json( # type: ignore[reportDeprecated] indent=indent, include=include, exclude=exclude, - by_alias=by_alias, + by_alias=by_alias if by_alias is not None else False, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, exclude_none=exclude_none, From 39cfb8c08d5fa021a2816e3aa586d4187816a33d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 18 Sep 2025 14:58:36 +0000 Subject: [PATCH 316/399] chore(types): change optional parameter type from NotGiven to Omit --- src/writerai/__init__.py | 4 +- src/writerai/_base_client.py | 18 +- src/writerai/_client.py | 16 +- src/writerai/_qs.py | 14 +- src/writerai/_types.py | 29 +-- src/writerai/_utils/_transform.py | 4 +- src/writerai/_utils/_utils.py | 8 +- .../resources/applications/applications.py | 54 ++--- src/writerai/resources/applications/graphs.py | 10 +- src/writerai/resources/applications/jobs.py | 30 +-- src/writerai/resources/chat.py | 186 +++++++++--------- src/writerai/resources/completions.py | 122 ++++++------ src/writerai/resources/files.py | 54 ++--- src/writerai/resources/graphs.py | 122 ++++++------ src/writerai/resources/models.py | 6 +- src/writerai/resources/tools/comprehend.py | 6 +- src/writerai/resources/tools/tools.py | 70 +++---- src/writerai/resources/translation.py | 6 +- src/writerai/resources/vision.py | 6 +- tests/test_transform.py | 11 +- 20 files changed, 396 insertions(+), 380 deletions(-) diff --git a/src/writerai/__init__.py b/src/writerai/__init__.py index c63a1e2c..cc744dd2 100644 --- a/src/writerai/__init__.py +++ b/src/writerai/__init__.py @@ -3,7 +3,7 @@ import typing as _t from . import types -from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes +from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes, omit, not_given from ._utils import file_from_path from ._client import Client, Stream, Writer, Timeout, Transport, AsyncClient, AsyncStream, AsyncWriter, RequestOptions from ._models import BaseModel @@ -38,7 +38,9 @@ "ProxiesTypes", "NotGiven", "NOT_GIVEN", + "not_given", "Omit", + "omit", "WriterError", "APIError", "APIStatusError", diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index d251c921..1bee460f 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -42,7 +42,6 @@ from ._qs import Querystring from ._files import to_httpx_files, async_to_httpx_files from ._types import ( - NOT_GIVEN, Body, Omit, Query, @@ -57,6 +56,7 @@ RequestOptions, HttpxRequestFiles, ModelBuilderProtocol, + not_given, ) from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping from ._compat import PYDANTIC_V1, model_copy, model_dump @@ -145,9 +145,9 @@ def __init__( def __init__( self, *, - url: URL | NotGiven = NOT_GIVEN, - json: Body | NotGiven = NOT_GIVEN, - params: Query | NotGiven = NOT_GIVEN, + url: URL | NotGiven = not_given, + json: Body | NotGiven = not_given, + params: Query | NotGiven = not_given, ) -> None: self.url = url self.json = json @@ -595,7 +595,7 @@ def _maybe_override_cast_to(self, cast_to: type[ResponseT], options: FinalReques # we internally support defining a temporary header to override the # default `cast_to` type for use with `.with_raw_response` and `.with_streaming_response` # see _response.py for implementation details - override_cast_to = headers.pop(OVERRIDE_CAST_TO_HEADER, NOT_GIVEN) + override_cast_to = headers.pop(OVERRIDE_CAST_TO_HEADER, not_given) if is_given(override_cast_to): options.headers = headers return cast(Type[ResponseT], override_cast_to) @@ -825,7 +825,7 @@ def __init__( version: str, base_url: str | URL, max_retries: int = DEFAULT_MAX_RETRIES, - timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | Timeout | None | NotGiven = not_given, http_client: httpx.Client | None = None, custom_headers: Mapping[str, str] | None = None, custom_query: Mapping[str, object] | None = None, @@ -1356,7 +1356,7 @@ def __init__( base_url: str | URL, _strict_response_validation: bool, max_retries: int = DEFAULT_MAX_RETRIES, - timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | Timeout | None | NotGiven = not_given, http_client: httpx.AsyncClient | None = None, custom_headers: Mapping[str, str] | None = None, custom_query: Mapping[str, object] | None = None, @@ -1818,8 +1818,8 @@ def make_request_options( extra_query: Query | None = None, extra_body: Body | None = None, idempotency_key: str | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - post_parser: PostParser | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + post_parser: PostParser | NotGiven = not_given, ) -> RequestOptions: """Create a dict of type RequestOptions without keys of NotGiven values.""" options: RequestOptions = {} diff --git a/src/writerai/_client.py b/src/writerai/_client.py index a58c7fca..a6f38d8f 100644 --- a/src/writerai/_client.py +++ b/src/writerai/_client.py @@ -3,7 +3,7 @@ from __future__ import annotations import os -from typing import Any, Union, Mapping +from typing import Any, Mapping from typing_extensions import Self, override import httpx @@ -11,13 +11,13 @@ from . import _exceptions from ._qs import Querystring from ._types import ( - NOT_GIVEN, Omit, Timeout, NotGiven, Transport, ProxiesTypes, RequestOptions, + not_given, ) from ._utils import is_given, get_async_library from ._version import __version__ @@ -56,7 +56,7 @@ def __init__( *, api_key: str | None = None, base_url: str | httpx.URL | None = None, - timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN, + timeout: float | Timeout | None | NotGiven = not_given, max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, default_query: Mapping[str, object] | None = None, @@ -141,9 +141,9 @@ def copy( *, api_key: str | None = None, base_url: str | httpx.URL | None = None, - timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | Timeout | None | NotGiven = not_given, http_client: httpx.Client | None = None, - max_retries: int | NotGiven = NOT_GIVEN, + max_retries: int | NotGiven = not_given, default_headers: Mapping[str, str] | None = None, set_default_headers: Mapping[str, str] | None = None, default_query: Mapping[str, object] | None = None, @@ -242,7 +242,7 @@ def __init__( *, api_key: str | None = None, base_url: str | httpx.URL | None = None, - timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN, + timeout: float | Timeout | None | NotGiven = not_given, max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, default_query: Mapping[str, object] | None = None, @@ -327,9 +327,9 @@ def copy( *, api_key: str | None = None, base_url: str | httpx.URL | None = None, - timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | Timeout | None | NotGiven = not_given, http_client: httpx.AsyncClient | None = None, - max_retries: int | NotGiven = NOT_GIVEN, + max_retries: int | NotGiven = not_given, default_headers: Mapping[str, str] | None = None, set_default_headers: Mapping[str, str] | None = None, default_query: Mapping[str, object] | None = None, diff --git a/src/writerai/_qs.py b/src/writerai/_qs.py index 274320ca..ada6fd3f 100644 --- a/src/writerai/_qs.py +++ b/src/writerai/_qs.py @@ -4,7 +4,7 @@ from urllib.parse import parse_qs, urlencode from typing_extensions import Literal, get_args -from ._types import NOT_GIVEN, NotGiven, NotGivenOr +from ._types import NotGiven, not_given from ._utils import flatten _T = TypeVar("_T") @@ -41,8 +41,8 @@ def stringify( self, params: Params, *, - array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN, - nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN, + array_format: ArrayFormat | NotGiven = not_given, + nested_format: NestedFormat | NotGiven = not_given, ) -> str: return urlencode( self.stringify_items( @@ -56,8 +56,8 @@ def stringify_items( self, params: Params, *, - array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN, - nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN, + array_format: ArrayFormat | NotGiven = not_given, + nested_format: NestedFormat | NotGiven = not_given, ) -> list[tuple[str, str]]: opts = Options( qs=self, @@ -143,8 +143,8 @@ def __init__( self, qs: Querystring = _qs, *, - array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN, - nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN, + array_format: ArrayFormat | NotGiven = not_given, + nested_format: NestedFormat | NotGiven = not_given, ) -> None: self.array_format = qs.array_format if isinstance(array_format, NotGiven) else array_format self.nested_format = qs.nested_format if isinstance(nested_format, NotGiven) else nested_format diff --git a/src/writerai/_types.py b/src/writerai/_types.py index 31170df3..146fcfb6 100644 --- a/src/writerai/_types.py +++ b/src/writerai/_types.py @@ -117,18 +117,21 @@ class RequestOptions(TypedDict, total=False): # Sentinel class used until PEP 0661 is accepted class NotGiven: """ - A sentinel singleton class used to distinguish omitted keyword arguments - from those passed in with the value None (which may have different behavior). + For parameters with a meaningful None value, we need to distinguish between + the user explicitly passing None, and the user not passing the parameter at + all. + + User code shouldn't need to use not_given directly. For example: ```py - def get(timeout: Union[int, NotGiven, None] = NotGiven()) -> Response: ... + def create(timeout: Timeout | None | NotGiven = not_given): ... - get(timeout=1) # 1s timeout - get(timeout=None) # No timeout - get() # Default timeout behavior, which may not be statically known at the method definition. + create(timeout=1) # 1s timeout + create(timeout=None) # No timeout + create() # Default timeout behavior ``` """ @@ -140,13 +143,14 @@ def __repr__(self) -> str: return "NOT_GIVEN" -NotGivenOr = Union[_T, NotGiven] +not_given = NotGiven() +# for backwards compatibility: NOT_GIVEN = NotGiven() class Omit: - """In certain situations you need to be able to represent a case where a default value has - to be explicitly removed and `None` is not an appropriate substitute, for example: + """ + To explicitly omit something from being sent in a request, use `omit`. ```py # as the default `Content-Type` header is `application/json` that will be sent @@ -156,8 +160,8 @@ class Omit: # to look something like: 'multipart/form-data; boundary=0d8382fcf5f8c3be01ca2e11002d2983' client.post(..., headers={"Content-Type": "multipart/form-data"}) - # instead you can remove the default `application/json` header by passing Omit - client.post(..., headers={"Content-Type": Omit()}) + # instead you can remove the default `application/json` header by passing omit + client.post(..., headers={"Content-Type": omit}) ``` """ @@ -165,6 +169,9 @@ def __bool__(self) -> Literal[False]: return False +omit = Omit() + + @runtime_checkable class ModelBuilderProtocol(Protocol): @classmethod diff --git a/src/writerai/_utils/_transform.py b/src/writerai/_utils/_transform.py index c19124f0..52075492 100644 --- a/src/writerai/_utils/_transform.py +++ b/src/writerai/_utils/_transform.py @@ -268,7 +268,7 @@ def _transform_typeddict( annotations = get_type_hints(expected_type, include_extras=True) for key, value in data.items(): if not is_given(value): - # we don't need to include `NotGiven` values here as they'll + # we don't need to include omitted values here as they'll # be stripped out before the request is sent anyway continue @@ -434,7 +434,7 @@ async def _async_transform_typeddict( annotations = get_type_hints(expected_type, include_extras=True) for key, value in data.items(): if not is_given(value): - # we don't need to include `NotGiven` values here as they'll + # we don't need to include omitted values here as they'll # be stripped out before the request is sent anyway continue diff --git a/src/writerai/_utils/_utils.py b/src/writerai/_utils/_utils.py index f0818595..50d59269 100644 --- a/src/writerai/_utils/_utils.py +++ b/src/writerai/_utils/_utils.py @@ -21,7 +21,7 @@ import sniffio -from .._types import NotGiven, FileTypes, NotGivenOr, HeadersLike +from .._types import Omit, NotGiven, FileTypes, HeadersLike _T = TypeVar("_T") _TupleT = TypeVar("_TupleT", bound=Tuple[object, ...]) @@ -63,7 +63,7 @@ def _extract_items( try: key = path[index] except IndexError: - if isinstance(obj, NotGiven): + if not is_given(obj): # no value was provided - we can safely ignore return [] @@ -126,8 +126,8 @@ def _extract_items( return [] -def is_given(obj: NotGivenOr[_T]) -> TypeGuard[_T]: - return not isinstance(obj, NotGiven) +def is_given(obj: _T | NotGiven | Omit) -> TypeGuard[_T]: + return not isinstance(obj, NotGiven) and not isinstance(obj, Omit) # Type safe methods for narrowing types with TypeVars. diff --git a/src/writerai/resources/applications/applications.py b/src/writerai/resources/applications/applications.py index 4a0e5f6d..d58d476f 100644 --- a/src/writerai/resources/applications/applications.py +++ b/src/writerai/resources/applications/applications.py @@ -24,7 +24,7 @@ AsyncGraphsResourceWithStreamingResponse, ) from ...types import application_list_params, application_generate_content_params -from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given from ..._utils import required_args, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource @@ -82,7 +82,7 @@ def retrieve( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ApplicationRetrieveResponse: """ Retrieves detailed information for a specific no-code agent (formerly called @@ -110,17 +110,17 @@ def retrieve( def list( self, *, - after: str | NotGiven = NOT_GIVEN, - before: str | NotGiven = NOT_GIVEN, - limit: int | NotGiven = NOT_GIVEN, - order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, - type: Literal["generation"] | NotGiven = NOT_GIVEN, + after: str | Omit = omit, + before: str | Omit = omit, + limit: int | Omit = omit, + order: Literal["asc", "desc"] | Omit = omit, + type: Literal["generation"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SyncCursorPage[ApplicationListResponse]: """ Retrieves a paginated list of no-code agents (formerly called no-code @@ -173,13 +173,13 @@ def generate_content( application_id: str, *, inputs: Iterable[application_generate_content_params.Input], - stream: Literal[False] | NotGiven = NOT_GIVEN, + stream: Literal[False] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ApplicationGenerateContentResponse: """ Generate content from an existing no-code agent (formerly called no-code @@ -211,7 +211,7 @@ def generate_content( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Stream[ApplicationGenerateContentChunk]: """ Generate content from an existing no-code agent (formerly called no-code @@ -243,7 +243,7 @@ def generate_content( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ApplicationGenerateContentResponse | Stream[ApplicationGenerateContentChunk]: """ Generate content from an existing no-code agent (formerly called no-code @@ -269,13 +269,13 @@ def generate_content( application_id: str, *, inputs: Iterable[application_generate_content_params.Input], - stream: Literal[False] | Literal[True] | NotGiven = NOT_GIVEN, + stream: Literal[False] | Literal[True] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ApplicationGenerateContentResponse | Stream[ApplicationGenerateContentChunk]: if not application_id: raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") @@ -336,7 +336,7 @@ async def retrieve( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ApplicationRetrieveResponse: """ Retrieves detailed information for a specific no-code agent (formerly called @@ -364,17 +364,17 @@ async def retrieve( def list( self, *, - after: str | NotGiven = NOT_GIVEN, - before: str | NotGiven = NOT_GIVEN, - limit: int | NotGiven = NOT_GIVEN, - order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, - type: Literal["generation"] | NotGiven = NOT_GIVEN, + after: str | Omit = omit, + before: str | Omit = omit, + limit: int | Omit = omit, + order: Literal["asc", "desc"] | Omit = omit, + type: Literal["generation"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AsyncPaginator[ApplicationListResponse, AsyncCursorPage[ApplicationListResponse]]: """ Retrieves a paginated list of no-code agents (formerly called no-code @@ -427,13 +427,13 @@ async def generate_content( application_id: str, *, inputs: Iterable[application_generate_content_params.Input], - stream: Literal[False] | NotGiven = NOT_GIVEN, + stream: Literal[False] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ApplicationGenerateContentResponse: """ Generate content from an existing no-code agent (formerly called no-code @@ -465,7 +465,7 @@ async def generate_content( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AsyncStream[ApplicationGenerateContentChunk]: """ Generate content from an existing no-code agent (formerly called no-code @@ -497,7 +497,7 @@ async def generate_content( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ApplicationGenerateContentResponse | AsyncStream[ApplicationGenerateContentChunk]: """ Generate content from an existing no-code agent (formerly called no-code @@ -523,13 +523,13 @@ async def generate_content( application_id: str, *, inputs: Iterable[application_generate_content_params.Input], - stream: Literal[False] | Literal[True] | NotGiven = NOT_GIVEN, + stream: Literal[False] | Literal[True] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ApplicationGenerateContentResponse | AsyncStream[ApplicationGenerateContentChunk]: if not application_id: raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") diff --git a/src/writerai/resources/applications/graphs.py b/src/writerai/resources/applications/graphs.py index cff8d7c6..2ef559a4 100644 --- a/src/writerai/resources/applications/graphs.py +++ b/src/writerai/resources/applications/graphs.py @@ -4,7 +4,7 @@ import httpx -from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven, SequenceNotStr +from ..._types import Body, Query, Headers, NotGiven, SequenceNotStr, not_given from ..._utils import maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource @@ -51,7 +51,7 @@ def update( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ApplicationGraphsResponse: """ Updates the list of Knowledge Graphs associated with a no-code chat agent. @@ -89,7 +89,7 @@ def list( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ApplicationGraphsResponse: """ Retrieve Knowledge Graphs associated with a no-code agent that has chat @@ -145,7 +145,7 @@ async def update( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ApplicationGraphsResponse: """ Updates the list of Knowledge Graphs associated with a no-code chat agent. @@ -183,7 +183,7 @@ async def list( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ApplicationGraphsResponse: """ Retrieve Knowledge Graphs associated with a no-code agent that has chat diff --git a/src/writerai/resources/applications/jobs.py b/src/writerai/resources/applications/jobs.py index 6a277cc0..f27f6a3e 100644 --- a/src/writerai/resources/applications/jobs.py +++ b/src/writerai/resources/applications/jobs.py @@ -7,7 +7,7 @@ import httpx -from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given from ..._utils import maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource @@ -57,7 +57,7 @@ def create( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> JobCreateResponse: """ Generate content asynchronously from an existing no-code agent (formerly called @@ -94,7 +94,7 @@ def retrieve( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ApplicationGenerateAsyncResponse: """ Retrieves a single job created via the Async API. @@ -122,15 +122,15 @@ def list( self, application_id: str, *, - limit: int | NotGiven = NOT_GIVEN, - offset: int | NotGiven = NOT_GIVEN, - status: Literal["in_progress", "failed", "completed"] | NotGiven = NOT_GIVEN, + limit: int | Omit = omit, + offset: int | Omit = omit, + status: Literal["in_progress", "failed", "completed"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SyncApplicationJobsOffset[ApplicationGenerateAsyncResponse]: """ Retrieve all jobs created via the async API, linked to the provided application @@ -182,7 +182,7 @@ def retry( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> JobRetryResponse: """ Re-triggers the async execution of a single job previously created via the Async @@ -238,7 +238,7 @@ async def create( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> JobCreateResponse: """ Generate content asynchronously from an existing no-code agent (formerly called @@ -275,7 +275,7 @@ async def retrieve( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ApplicationGenerateAsyncResponse: """ Retrieves a single job created via the Async API. @@ -303,15 +303,15 @@ def list( self, application_id: str, *, - limit: int | NotGiven = NOT_GIVEN, - offset: int | NotGiven = NOT_GIVEN, - status: Literal["in_progress", "failed", "completed"] | NotGiven = NOT_GIVEN, + limit: int | Omit = omit, + offset: int | Omit = omit, + status: Literal["in_progress", "failed", "completed"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AsyncPaginator[ApplicationGenerateAsyncResponse, AsyncApplicationJobsOffset[ApplicationGenerateAsyncResponse]]: """ Retrieve all jobs created via the async API, linked to the provided application @@ -363,7 +363,7 @@ async def retry( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> JobRetryResponse: """ Re-triggers the async execution of a single job previously created via the Async diff --git a/src/writerai/resources/chat.py b/src/writerai/resources/chat.py index d209a84e..52835ec2 100644 --- a/src/writerai/resources/chat.py +++ b/src/writerai/resources/chat.py @@ -8,7 +8,7 @@ import httpx from ..types import chat_chat_params -from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, SequenceNotStr +from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given from .._utils import required_args, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -53,23 +53,23 @@ def chat( *, messages: Iterable[chat_chat_params.Message], model: str, - logprobs: bool | NotGiven = NOT_GIVEN, - max_tokens: int | NotGiven = NOT_GIVEN, - n: int | NotGiven = NOT_GIVEN, - response_format: chat_chat_params.ResponseFormat | NotGiven = NOT_GIVEN, - stop: Union[SequenceNotStr[str], str] | NotGiven = NOT_GIVEN, - stream: Literal[False] | NotGiven = NOT_GIVEN, - stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, - temperature: float | NotGiven = NOT_GIVEN, - tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[ToolParam] | NotGiven = NOT_GIVEN, - top_p: float | NotGiven = NOT_GIVEN, + logprobs: bool | Omit = omit, + max_tokens: int | Omit = omit, + n: int | Omit = omit, + response_format: chat_chat_params.ResponseFormat | Omit = omit, + stop: Union[SequenceNotStr[str], str] | Omit = omit, + stream: Literal[False] | Omit = omit, + stream_options: chat_chat_params.StreamOptions | Omit = omit, + temperature: float | Omit = omit, + tool_choice: chat_chat_params.ToolChoice | Omit = omit, + tools: Iterable[ToolParam] | Omit = omit, + top_p: float | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ChatCompletion: """Generate a chat completion based on the provided messages. @@ -161,22 +161,22 @@ def chat( messages: Iterable[chat_chat_params.Message], model: str, stream: Literal[True], - logprobs: bool | NotGiven = NOT_GIVEN, - max_tokens: int | NotGiven = NOT_GIVEN, - n: int | NotGiven = NOT_GIVEN, - response_format: chat_chat_params.ResponseFormat | NotGiven = NOT_GIVEN, - stop: Union[SequenceNotStr[str], str] | NotGiven = NOT_GIVEN, - stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, - temperature: float | NotGiven = NOT_GIVEN, - tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[ToolParam] | NotGiven = NOT_GIVEN, - top_p: float | NotGiven = NOT_GIVEN, + logprobs: bool | Omit = omit, + max_tokens: int | Omit = omit, + n: int | Omit = omit, + response_format: chat_chat_params.ResponseFormat | Omit = omit, + stop: Union[SequenceNotStr[str], str] | Omit = omit, + stream_options: chat_chat_params.StreamOptions | Omit = omit, + temperature: float | Omit = omit, + tool_choice: chat_chat_params.ToolChoice | Omit = omit, + tools: Iterable[ToolParam] | Omit = omit, + top_p: float | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Stream[ChatCompletionChunk]: """Generate a chat completion based on the provided messages. @@ -268,22 +268,22 @@ def chat( messages: Iterable[chat_chat_params.Message], model: str, stream: bool, - logprobs: bool | NotGiven = NOT_GIVEN, - max_tokens: int | NotGiven = NOT_GIVEN, - n: int | NotGiven = NOT_GIVEN, - response_format: chat_chat_params.ResponseFormat | NotGiven = NOT_GIVEN, - stop: Union[SequenceNotStr[str], str] | NotGiven = NOT_GIVEN, - stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, - temperature: float | NotGiven = NOT_GIVEN, - tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[ToolParam] | NotGiven = NOT_GIVEN, - top_p: float | NotGiven = NOT_GIVEN, + logprobs: bool | Omit = omit, + max_tokens: int | Omit = omit, + n: int | Omit = omit, + response_format: chat_chat_params.ResponseFormat | Omit = omit, + stop: Union[SequenceNotStr[str], str] | Omit = omit, + stream_options: chat_chat_params.StreamOptions | Omit = omit, + temperature: float | Omit = omit, + tool_choice: chat_chat_params.ToolChoice | Omit = omit, + tools: Iterable[ToolParam] | Omit = omit, + top_p: float | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ChatCompletion | Stream[ChatCompletionChunk]: """Generate a chat completion based on the provided messages. @@ -374,23 +374,23 @@ def chat( *, messages: Iterable[chat_chat_params.Message], model: str, - logprobs: bool | NotGiven = NOT_GIVEN, - max_tokens: int | NotGiven = NOT_GIVEN, - n: int | NotGiven = NOT_GIVEN, - response_format: chat_chat_params.ResponseFormat | NotGiven = NOT_GIVEN, - stop: Union[SequenceNotStr[str], str] | NotGiven = NOT_GIVEN, - stream: Literal[False] | Literal[True] | NotGiven = NOT_GIVEN, - stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, - temperature: float | NotGiven = NOT_GIVEN, - tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[ToolParam] | NotGiven = NOT_GIVEN, - top_p: float | NotGiven = NOT_GIVEN, + logprobs: bool | Omit = omit, + max_tokens: int | Omit = omit, + n: int | Omit = omit, + response_format: chat_chat_params.ResponseFormat | Omit = omit, + stop: Union[SequenceNotStr[str], str] | Omit = omit, + stream: Literal[False] | Literal[True] | Omit = omit, + stream_options: chat_chat_params.StreamOptions | Omit = omit, + temperature: float | Omit = omit, + tool_choice: chat_chat_params.ToolChoice | Omit = omit, + tools: Iterable[ToolParam] | Omit = omit, + top_p: float | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ChatCompletion | Stream[ChatCompletionChunk]: return self._post( "/v1/chat", @@ -447,23 +447,23 @@ async def chat( *, messages: Iterable[chat_chat_params.Message], model: str, - logprobs: bool | NotGiven = NOT_GIVEN, - max_tokens: int | NotGiven = NOT_GIVEN, - n: int | NotGiven = NOT_GIVEN, - response_format: chat_chat_params.ResponseFormat | NotGiven = NOT_GIVEN, - stop: Union[SequenceNotStr[str], str] | NotGiven = NOT_GIVEN, - stream: Literal[False] | NotGiven = NOT_GIVEN, - stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, - temperature: float | NotGiven = NOT_GIVEN, - tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[ToolParam] | NotGiven = NOT_GIVEN, - top_p: float | NotGiven = NOT_GIVEN, + logprobs: bool | Omit = omit, + max_tokens: int | Omit = omit, + n: int | Omit = omit, + response_format: chat_chat_params.ResponseFormat | Omit = omit, + stop: Union[SequenceNotStr[str], str] | Omit = omit, + stream: Literal[False] | Omit = omit, + stream_options: chat_chat_params.StreamOptions | Omit = omit, + temperature: float | Omit = omit, + tool_choice: chat_chat_params.ToolChoice | Omit = omit, + tools: Iterable[ToolParam] | Omit = omit, + top_p: float | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ChatCompletion: """Generate a chat completion based on the provided messages. @@ -555,22 +555,22 @@ async def chat( messages: Iterable[chat_chat_params.Message], model: str, stream: Literal[True], - logprobs: bool | NotGiven = NOT_GIVEN, - max_tokens: int | NotGiven = NOT_GIVEN, - n: int | NotGiven = NOT_GIVEN, - response_format: chat_chat_params.ResponseFormat | NotGiven = NOT_GIVEN, - stop: Union[SequenceNotStr[str], str] | NotGiven = NOT_GIVEN, - stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, - temperature: float | NotGiven = NOT_GIVEN, - tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[ToolParam] | NotGiven = NOT_GIVEN, - top_p: float | NotGiven = NOT_GIVEN, + logprobs: bool | Omit = omit, + max_tokens: int | Omit = omit, + n: int | Omit = omit, + response_format: chat_chat_params.ResponseFormat | Omit = omit, + stop: Union[SequenceNotStr[str], str] | Omit = omit, + stream_options: chat_chat_params.StreamOptions | Omit = omit, + temperature: float | Omit = omit, + tool_choice: chat_chat_params.ToolChoice | Omit = omit, + tools: Iterable[ToolParam] | Omit = omit, + top_p: float | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AsyncStream[ChatCompletionChunk]: """Generate a chat completion based on the provided messages. @@ -662,22 +662,22 @@ async def chat( messages: Iterable[chat_chat_params.Message], model: str, stream: bool, - logprobs: bool | NotGiven = NOT_GIVEN, - max_tokens: int | NotGiven = NOT_GIVEN, - n: int | NotGiven = NOT_GIVEN, - response_format: chat_chat_params.ResponseFormat | NotGiven = NOT_GIVEN, - stop: Union[SequenceNotStr[str], str] | NotGiven = NOT_GIVEN, - stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, - temperature: float | NotGiven = NOT_GIVEN, - tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[ToolParam] | NotGiven = NOT_GIVEN, - top_p: float | NotGiven = NOT_GIVEN, + logprobs: bool | Omit = omit, + max_tokens: int | Omit = omit, + n: int | Omit = omit, + response_format: chat_chat_params.ResponseFormat | Omit = omit, + stop: Union[SequenceNotStr[str], str] | Omit = omit, + stream_options: chat_chat_params.StreamOptions | Omit = omit, + temperature: float | Omit = omit, + tool_choice: chat_chat_params.ToolChoice | Omit = omit, + tools: Iterable[ToolParam] | Omit = omit, + top_p: float | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ChatCompletion | AsyncStream[ChatCompletionChunk]: """Generate a chat completion based on the provided messages. @@ -768,23 +768,23 @@ async def chat( *, messages: Iterable[chat_chat_params.Message], model: str, - logprobs: bool | NotGiven = NOT_GIVEN, - max_tokens: int | NotGiven = NOT_GIVEN, - n: int | NotGiven = NOT_GIVEN, - response_format: chat_chat_params.ResponseFormat | NotGiven = NOT_GIVEN, - stop: Union[SequenceNotStr[str], str] | NotGiven = NOT_GIVEN, - stream: Literal[False] | Literal[True] | NotGiven = NOT_GIVEN, - stream_options: chat_chat_params.StreamOptions | NotGiven = NOT_GIVEN, - temperature: float | NotGiven = NOT_GIVEN, - tool_choice: chat_chat_params.ToolChoice | NotGiven = NOT_GIVEN, - tools: Iterable[ToolParam] | NotGiven = NOT_GIVEN, - top_p: float | NotGiven = NOT_GIVEN, + logprobs: bool | Omit = omit, + max_tokens: int | Omit = omit, + n: int | Omit = omit, + response_format: chat_chat_params.ResponseFormat | Omit = omit, + stop: Union[SequenceNotStr[str], str] | Omit = omit, + stream: Literal[False] | Literal[True] | Omit = omit, + stream_options: chat_chat_params.StreamOptions | Omit = omit, + temperature: float | Omit = omit, + tool_choice: chat_chat_params.ToolChoice | Omit = omit, + tools: Iterable[ToolParam] | Omit = omit, + top_p: float | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ChatCompletion | AsyncStream[ChatCompletionChunk]: return await self._post( "/v1/chat", diff --git a/src/writerai/resources/completions.py b/src/writerai/resources/completions.py index 417eecc9..382cb6a0 100644 --- a/src/writerai/resources/completions.py +++ b/src/writerai/resources/completions.py @@ -8,7 +8,7 @@ import httpx from ..types import completion_create_params -from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, SequenceNotStr +from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given from .._utils import required_args, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -52,19 +52,19 @@ def create( *, model: str, prompt: str, - best_of: int | NotGiven = NOT_GIVEN, - max_tokens: int | NotGiven = NOT_GIVEN, - random_seed: int | NotGiven = NOT_GIVEN, - stop: Union[SequenceNotStr[str], str] | NotGiven = NOT_GIVEN, - stream: Literal[False] | NotGiven = NOT_GIVEN, - temperature: float | NotGiven = NOT_GIVEN, - top_p: float | NotGiven = NOT_GIVEN, + best_of: int | Omit = omit, + max_tokens: int | Omit = omit, + random_seed: int | Omit = omit, + stop: Union[SequenceNotStr[str], str] | Omit = omit, + stream: Literal[False] | Omit = omit, + temperature: float | Omit = omit, + top_p: float | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Completion: """Generate text completions using the specified model and prompt. @@ -118,18 +118,18 @@ def create( model: str, prompt: str, stream: Literal[True], - best_of: int | NotGiven = NOT_GIVEN, - max_tokens: int | NotGiven = NOT_GIVEN, - random_seed: int | NotGiven = NOT_GIVEN, - stop: Union[SequenceNotStr[str], str] | NotGiven = NOT_GIVEN, - temperature: float | NotGiven = NOT_GIVEN, - top_p: float | NotGiven = NOT_GIVEN, + best_of: int | Omit = omit, + max_tokens: int | Omit = omit, + random_seed: int | Omit = omit, + stop: Union[SequenceNotStr[str], str] | Omit = omit, + temperature: float | Omit = omit, + top_p: float | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Stream[CompletionChunk]: """Generate text completions using the specified model and prompt. @@ -183,18 +183,18 @@ def create( model: str, prompt: str, stream: bool, - best_of: int | NotGiven = NOT_GIVEN, - max_tokens: int | NotGiven = NOT_GIVEN, - random_seed: int | NotGiven = NOT_GIVEN, - stop: Union[SequenceNotStr[str], str] | NotGiven = NOT_GIVEN, - temperature: float | NotGiven = NOT_GIVEN, - top_p: float | NotGiven = NOT_GIVEN, + best_of: int | Omit = omit, + max_tokens: int | Omit = omit, + random_seed: int | Omit = omit, + stop: Union[SequenceNotStr[str], str] | Omit = omit, + temperature: float | Omit = omit, + top_p: float | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Completion | Stream[CompletionChunk]: """Generate text completions using the specified model and prompt. @@ -247,19 +247,19 @@ def create( *, model: str, prompt: str, - best_of: int | NotGiven = NOT_GIVEN, - max_tokens: int | NotGiven = NOT_GIVEN, - random_seed: int | NotGiven = NOT_GIVEN, - stop: Union[SequenceNotStr[str], str] | NotGiven = NOT_GIVEN, - stream: Literal[False] | Literal[True] | NotGiven = NOT_GIVEN, - temperature: float | NotGiven = NOT_GIVEN, - top_p: float | NotGiven = NOT_GIVEN, + best_of: int | Omit = omit, + max_tokens: int | Omit = omit, + random_seed: int | Omit = omit, + stop: Union[SequenceNotStr[str], str] | Omit = omit, + stream: Literal[False] | Literal[True] | Omit = omit, + temperature: float | Omit = omit, + top_p: float | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Completion | Stream[CompletionChunk]: return self._post( "/v1/completions", @@ -314,19 +314,19 @@ async def create( *, model: str, prompt: str, - best_of: int | NotGiven = NOT_GIVEN, - max_tokens: int | NotGiven = NOT_GIVEN, - random_seed: int | NotGiven = NOT_GIVEN, - stop: Union[SequenceNotStr[str], str] | NotGiven = NOT_GIVEN, - stream: Literal[False] | NotGiven = NOT_GIVEN, - temperature: float | NotGiven = NOT_GIVEN, - top_p: float | NotGiven = NOT_GIVEN, + best_of: int | Omit = omit, + max_tokens: int | Omit = omit, + random_seed: int | Omit = omit, + stop: Union[SequenceNotStr[str], str] | Omit = omit, + stream: Literal[False] | Omit = omit, + temperature: float | Omit = omit, + top_p: float | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Completion: """Generate text completions using the specified model and prompt. @@ -380,18 +380,18 @@ async def create( model: str, prompt: str, stream: Literal[True], - best_of: int | NotGiven = NOT_GIVEN, - max_tokens: int | NotGiven = NOT_GIVEN, - random_seed: int | NotGiven = NOT_GIVEN, - stop: Union[SequenceNotStr[str], str] | NotGiven = NOT_GIVEN, - temperature: float | NotGiven = NOT_GIVEN, - top_p: float | NotGiven = NOT_GIVEN, + best_of: int | Omit = omit, + max_tokens: int | Omit = omit, + random_seed: int | Omit = omit, + stop: Union[SequenceNotStr[str], str] | Omit = omit, + temperature: float | Omit = omit, + top_p: float | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AsyncStream[CompletionChunk]: """Generate text completions using the specified model and prompt. @@ -445,18 +445,18 @@ async def create( model: str, prompt: str, stream: bool, - best_of: int | NotGiven = NOT_GIVEN, - max_tokens: int | NotGiven = NOT_GIVEN, - random_seed: int | NotGiven = NOT_GIVEN, - stop: Union[SequenceNotStr[str], str] | NotGiven = NOT_GIVEN, - temperature: float | NotGiven = NOT_GIVEN, - top_p: float | NotGiven = NOT_GIVEN, + best_of: int | Omit = omit, + max_tokens: int | Omit = omit, + random_seed: int | Omit = omit, + stop: Union[SequenceNotStr[str], str] | Omit = omit, + temperature: float | Omit = omit, + top_p: float | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Completion | AsyncStream[CompletionChunk]: """Generate text completions using the specified model and prompt. @@ -509,19 +509,19 @@ async def create( *, model: str, prompt: str, - best_of: int | NotGiven = NOT_GIVEN, - max_tokens: int | NotGiven = NOT_GIVEN, - random_seed: int | NotGiven = NOT_GIVEN, - stop: Union[SequenceNotStr[str], str] | NotGiven = NOT_GIVEN, - stream: Literal[False] | Literal[True] | NotGiven = NOT_GIVEN, - temperature: float | NotGiven = NOT_GIVEN, - top_p: float | NotGiven = NOT_GIVEN, + best_of: int | Omit = omit, + max_tokens: int | Omit = omit, + random_seed: int | Omit = omit, + stop: Union[SequenceNotStr[str], str] | Omit = omit, + stream: Literal[False] | Literal[True] | Omit = omit, + temperature: float | Omit = omit, + top_p: float | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Completion | AsyncStream[CompletionChunk]: return await self._post( "/v1/completions", diff --git a/src/writerai/resources/files.py b/src/writerai/resources/files.py index 458a1da5..cc7c1a78 100644 --- a/src/writerai/resources/files.py +++ b/src/writerai/resources/files.py @@ -7,7 +7,7 @@ import httpx from ..types import file_list_params, file_retry_params, file_upload_params -from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes, SequenceNotStr +from .._types import Body, Omit, Query, Headers, NotGiven, FileTypes, SequenceNotStr, omit, not_given from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -63,7 +63,7 @@ def retrieve( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> File: """ Retrieve detailed information about a specific file, including its metadata, @@ -91,19 +91,19 @@ def retrieve( def list( self, *, - after: str | NotGiven = NOT_GIVEN, - before: str | NotGiven = NOT_GIVEN, - file_types: str | NotGiven = NOT_GIVEN, - graph_id: str | NotGiven = NOT_GIVEN, - limit: int | NotGiven = NOT_GIVEN, - order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, - status: Literal["in_progress", "completed", "failed"] | NotGiven = NOT_GIVEN, + after: str | Omit = omit, + before: str | Omit = omit, + file_types: str | Omit = omit, + graph_id: str | Omit = omit, + limit: int | Omit = omit, + order: Literal["asc", "desc"] | Omit = omit, + status: Literal["in_progress", "completed", "failed"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SyncCursorPage[File]: """ Retrieve a paginated list of files with optional filtering by status, graph @@ -171,7 +171,7 @@ def delete( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> FileDeleteResponse: """Permanently delete a file from the system. @@ -205,7 +205,7 @@ def download( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> BinaryAPIResponse: """Download the binary content of a file. @@ -241,7 +241,7 @@ def retry( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> FileRetryResponse: """Retry processing of files that previously failed to process. @@ -278,7 +278,7 @@ def upload( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> File: """Upload a new file to the system. @@ -334,7 +334,7 @@ async def retrieve( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> File: """ Retrieve detailed information about a specific file, including its metadata, @@ -362,19 +362,19 @@ async def retrieve( def list( self, *, - after: str | NotGiven = NOT_GIVEN, - before: str | NotGiven = NOT_GIVEN, - file_types: str | NotGiven = NOT_GIVEN, - graph_id: str | NotGiven = NOT_GIVEN, - limit: int | NotGiven = NOT_GIVEN, - order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, - status: Literal["in_progress", "completed", "failed"] | NotGiven = NOT_GIVEN, + after: str | Omit = omit, + before: str | Omit = omit, + file_types: str | Omit = omit, + graph_id: str | Omit = omit, + limit: int | Omit = omit, + order: Literal["asc", "desc"] | Omit = omit, + status: Literal["in_progress", "completed", "failed"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AsyncPaginator[File, AsyncCursorPage[File]]: """ Retrieve a paginated list of files with optional filtering by status, graph @@ -442,7 +442,7 @@ async def delete( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> FileDeleteResponse: """Permanently delete a file from the system. @@ -476,7 +476,7 @@ async def download( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AsyncBinaryAPIResponse: """Download the binary content of a file. @@ -512,7 +512,7 @@ async def retry( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> FileRetryResponse: """Retry processing of files that previously failed to process. @@ -549,7 +549,7 @@ async def upload( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> File: """Upload a new file to the system. diff --git a/src/writerai/resources/graphs.py b/src/writerai/resources/graphs.py index 69ad92ed..fe13b4b2 100644 --- a/src/writerai/resources/graphs.py +++ b/src/writerai/resources/graphs.py @@ -14,7 +14,7 @@ graph_question_params, graph_add_file_to_graph_params, ) -from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, SequenceNotStr +from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given from .._utils import required_args, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -62,14 +62,14 @@ def with_streaming_response(self) -> GraphsResourceWithStreamingResponse: def create( self, *, - description: str | NotGiven = NOT_GIVEN, - name: str | NotGiven = NOT_GIVEN, + description: str | Omit = omit, + name: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> GraphCreateResponse: """ Create a new Knowledge Graph. @@ -113,7 +113,7 @@ def retrieve( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Graph: """ Retrieve a Knowledge Graph. @@ -141,15 +141,15 @@ def update( self, graph_id: str, *, - description: str | NotGiven = NOT_GIVEN, - name: str | NotGiven = NOT_GIVEN, - urls: Iterable[graph_update_params.URL] | NotGiven = NOT_GIVEN, + description: str | Omit = omit, + name: str | Omit = omit, + urls: Iterable[graph_update_params.URL] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> GraphUpdateResponse: """ Update the name and description of a Knowledge Graph. @@ -194,16 +194,16 @@ def update( def list( self, *, - after: str | NotGiven = NOT_GIVEN, - before: str | NotGiven = NOT_GIVEN, - limit: int | NotGiven = NOT_GIVEN, - order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, + after: str | Omit = omit, + before: str | Omit = omit, + limit: int | Omit = omit, + order: Literal["asc", "desc"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SyncCursorPage[Graph]: """ Retrieve a list of Knowledge Graphs. @@ -259,7 +259,7 @@ def delete( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> GraphDeleteResponse: """ Delete a Knowledge Graph. @@ -293,7 +293,7 @@ def add_file_to_graph( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> File: """ Add a file to a Knowledge Graph. @@ -326,15 +326,15 @@ def question( *, graph_ids: SequenceNotStr[str], question: str, - query_config: graph_question_params.QueryConfig | NotGiven = NOT_GIVEN, - stream: Literal[False] | NotGiven = NOT_GIVEN, - subqueries: bool | NotGiven = NOT_GIVEN, + query_config: graph_question_params.QueryConfig | Omit = omit, + stream: Literal[False] | Omit = omit, + subqueries: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Question: """ Ask a question to specified Knowledge Graphs. @@ -370,14 +370,14 @@ def question( graph_ids: SequenceNotStr[str], question: str, stream: Literal[True], - query_config: graph_question_params.QueryConfig | NotGiven = NOT_GIVEN, - subqueries: bool | NotGiven = NOT_GIVEN, + query_config: graph_question_params.QueryConfig | Omit = omit, + subqueries: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Stream[QuestionResponseChunk]: """ Ask a question to specified Knowledge Graphs. @@ -413,14 +413,14 @@ def question( graph_ids: SequenceNotStr[str], question: str, stream: bool, - query_config: graph_question_params.QueryConfig | NotGiven = NOT_GIVEN, - subqueries: bool | NotGiven = NOT_GIVEN, + query_config: graph_question_params.QueryConfig | Omit = omit, + subqueries: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Question | Stream[QuestionResponseChunk]: """ Ask a question to specified Knowledge Graphs. @@ -455,15 +455,15 @@ def question( *, graph_ids: SequenceNotStr[str], question: str, - query_config: graph_question_params.QueryConfig | NotGiven = NOT_GIVEN, - stream: Literal[False] | Literal[True] | NotGiven = NOT_GIVEN, - subqueries: bool | NotGiven = NOT_GIVEN, + query_config: graph_question_params.QueryConfig | Omit = omit, + stream: Literal[False] | Literal[True] | Omit = omit, + subqueries: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Question | Stream[QuestionResponseChunk]: return self._post( "/v1/graphs/question", @@ -497,7 +497,7 @@ def remove_file_from_graph( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> GraphRemoveFileFromGraphResponse: """ Remove a file from a Knowledge Graph. @@ -547,14 +547,14 @@ def with_streaming_response(self) -> AsyncGraphsResourceWithStreamingResponse: async def create( self, *, - description: str | NotGiven = NOT_GIVEN, - name: str | NotGiven = NOT_GIVEN, + description: str | Omit = omit, + name: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> GraphCreateResponse: """ Create a new Knowledge Graph. @@ -598,7 +598,7 @@ async def retrieve( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Graph: """ Retrieve a Knowledge Graph. @@ -626,15 +626,15 @@ async def update( self, graph_id: str, *, - description: str | NotGiven = NOT_GIVEN, - name: str | NotGiven = NOT_GIVEN, - urls: Iterable[graph_update_params.URL] | NotGiven = NOT_GIVEN, + description: str | Omit = omit, + name: str | Omit = omit, + urls: Iterable[graph_update_params.URL] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> GraphUpdateResponse: """ Update the name and description of a Knowledge Graph. @@ -679,16 +679,16 @@ async def update( def list( self, *, - after: str | NotGiven = NOT_GIVEN, - before: str | NotGiven = NOT_GIVEN, - limit: int | NotGiven = NOT_GIVEN, - order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, + after: str | Omit = omit, + before: str | Omit = omit, + limit: int | Omit = omit, + order: Literal["asc", "desc"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AsyncPaginator[Graph, AsyncCursorPage[Graph]]: """ Retrieve a list of Knowledge Graphs. @@ -744,7 +744,7 @@ async def delete( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> GraphDeleteResponse: """ Delete a Knowledge Graph. @@ -778,7 +778,7 @@ async def add_file_to_graph( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> File: """ Add a file to a Knowledge Graph. @@ -813,15 +813,15 @@ async def question( *, graph_ids: SequenceNotStr[str], question: str, - query_config: graph_question_params.QueryConfig | NotGiven = NOT_GIVEN, - stream: Literal[False] | NotGiven = NOT_GIVEN, - subqueries: bool | NotGiven = NOT_GIVEN, + query_config: graph_question_params.QueryConfig | Omit = omit, + stream: Literal[False] | Omit = omit, + subqueries: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Question: """ Ask a question to specified Knowledge Graphs. @@ -857,14 +857,14 @@ async def question( graph_ids: SequenceNotStr[str], question: str, stream: Literal[True], - query_config: graph_question_params.QueryConfig | NotGiven = NOT_GIVEN, - subqueries: bool | NotGiven = NOT_GIVEN, + query_config: graph_question_params.QueryConfig | Omit = omit, + subqueries: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AsyncStream[QuestionResponseChunk]: """ Ask a question to specified Knowledge Graphs. @@ -900,14 +900,14 @@ async def question( graph_ids: SequenceNotStr[str], question: str, stream: bool, - query_config: graph_question_params.QueryConfig | NotGiven = NOT_GIVEN, - subqueries: bool | NotGiven = NOT_GIVEN, + query_config: graph_question_params.QueryConfig | Omit = omit, + subqueries: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Question | AsyncStream[QuestionResponseChunk]: """ Ask a question to specified Knowledge Graphs. @@ -942,15 +942,15 @@ async def question( *, graph_ids: SequenceNotStr[str], question: str, - query_config: graph_question_params.QueryConfig | NotGiven = NOT_GIVEN, - stream: Literal[False] | Literal[True] | NotGiven = NOT_GIVEN, - subqueries: bool | NotGiven = NOT_GIVEN, + query_config: graph_question_params.QueryConfig | Omit = omit, + stream: Literal[False] | Literal[True] | Omit = omit, + subqueries: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Question | AsyncStream[QuestionResponseChunk]: return await self._post( "/v1/graphs/question", @@ -984,7 +984,7 @@ async def remove_file_from_graph( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> GraphRemoveFileFromGraphResponse: """ Remove a file from a Knowledge Graph. diff --git a/src/writerai/resources/models.py b/src/writerai/resources/models.py index b7515656..d7bc28a6 100644 --- a/src/writerai/resources/models.py +++ b/src/writerai/resources/models.py @@ -4,7 +4,7 @@ import httpx -from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from .._types import Body, Query, Headers, NotGiven, not_given from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -47,7 +47,7 @@ def list( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ModelListResponse: """ Retrieve a list of available models that can be used for text generation, chat @@ -90,7 +90,7 @@ async def list( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ModelListResponse: """ Retrieve a list of available models that can be used for text generation, chat diff --git a/src/writerai/resources/tools/comprehend.py b/src/writerai/resources/tools/comprehend.py index cbae676a..07bb2b73 100644 --- a/src/writerai/resources/tools/comprehend.py +++ b/src/writerai/resources/tools/comprehend.py @@ -6,7 +6,7 @@ import httpx -from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ..._types import Body, Query, Headers, NotGiven, not_given from ..._utils import maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource @@ -53,7 +53,7 @@ def medical( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ComprehendMedicalResponse: """ Analyze unstructured medical text to extract entities labeled with standardized @@ -120,7 +120,7 @@ async def medical( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ComprehendMedicalResponse: """ Analyze unstructured medical text to extract entities labeled with standardized diff --git a/src/writerai/resources/tools/tools.py b/src/writerai/resources/tools/tools.py index 1f739424..73562f52 100644 --- a/src/writerai/resources/tools/tools.py +++ b/src/writerai/resources/tools/tools.py @@ -13,7 +13,7 @@ tool_web_search_params, tool_context_aware_splitting_params, ) -from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven, SequenceNotStr +from ..._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given from ..._utils import maybe_transform, async_maybe_transform from ..._compat import cached_property from .comprehend import ( @@ -73,7 +73,7 @@ def ai_detect( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ToolAIDetectResponse: """Detects if content is AI- or human-generated, with a confidence score. @@ -111,7 +111,7 @@ def context_aware_splitting( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ToolContextAwareSplittingResponse: """ Splits a long block of text (maximum 4000 words) into smaller chunks while @@ -157,7 +157,7 @@ def parse_pdf( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ToolParsePdfResponse: """ Parse PDF to other formats. @@ -187,7 +187,7 @@ def parse_pdf( def web_search( self, *, - chunks_per_source: int | NotGiven = NOT_GIVEN, + chunks_per_source: int | Omit = omit, country: Literal[ "afghanistan", "albania", @@ -356,24 +356,24 @@ def web_search( "zambia", "zimbabwe", ] - | NotGiven = NOT_GIVEN, - days: int | NotGiven = NOT_GIVEN, - exclude_domains: SequenceNotStr[str] | NotGiven = NOT_GIVEN, - include_answer: bool | NotGiven = NOT_GIVEN, - include_domains: SequenceNotStr[str] | NotGiven = NOT_GIVEN, - include_raw_content: Union[Literal["text", "markdown"], bool] | NotGiven = NOT_GIVEN, - max_results: int | NotGiven = NOT_GIVEN, - query: str | NotGiven = NOT_GIVEN, - search_depth: Literal["basic", "advanced"] | NotGiven = NOT_GIVEN, - stream: bool | NotGiven = NOT_GIVEN, - time_range: Literal["day", "week", "month", "year", "d", "w", "m", "y"] | NotGiven = NOT_GIVEN, - topic: Literal["general", "news"] | NotGiven = NOT_GIVEN, + | Omit = omit, + days: int | Omit = omit, + exclude_domains: SequenceNotStr[str] | Omit = omit, + include_answer: bool | Omit = omit, + include_domains: SequenceNotStr[str] | Omit = omit, + include_raw_content: Union[Literal["text", "markdown"], bool] | Omit = omit, + max_results: int | Omit = omit, + query: str | Omit = omit, + search_depth: Literal["basic", "advanced"] | Omit = omit, + stream: bool | Omit = omit, + time_range: Literal["day", "week", "month", "year", "d", "w", "m", "y"] | Omit = omit, + topic: Literal["general", "news"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ToolWebSearchResponse: """ Search the web for information about a given query and return relevant results @@ -491,7 +491,7 @@ async def ai_detect( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ToolAIDetectResponse: """Detects if content is AI- or human-generated, with a confidence score. @@ -529,7 +529,7 @@ async def context_aware_splitting( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ToolContextAwareSplittingResponse: """ Splits a long block of text (maximum 4000 words) into smaller chunks while @@ -575,7 +575,7 @@ async def parse_pdf( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ToolParsePdfResponse: """ Parse PDF to other formats. @@ -605,7 +605,7 @@ async def parse_pdf( async def web_search( self, *, - chunks_per_source: int | NotGiven = NOT_GIVEN, + chunks_per_source: int | Omit = omit, country: Literal[ "afghanistan", "albania", @@ -774,24 +774,24 @@ async def web_search( "zambia", "zimbabwe", ] - | NotGiven = NOT_GIVEN, - days: int | NotGiven = NOT_GIVEN, - exclude_domains: SequenceNotStr[str] | NotGiven = NOT_GIVEN, - include_answer: bool | NotGiven = NOT_GIVEN, - include_domains: SequenceNotStr[str] | NotGiven = NOT_GIVEN, - include_raw_content: Union[Literal["text", "markdown"], bool] | NotGiven = NOT_GIVEN, - max_results: int | NotGiven = NOT_GIVEN, - query: str | NotGiven = NOT_GIVEN, - search_depth: Literal["basic", "advanced"] | NotGiven = NOT_GIVEN, - stream: bool | NotGiven = NOT_GIVEN, - time_range: Literal["day", "week", "month", "year", "d", "w", "m", "y"] | NotGiven = NOT_GIVEN, - topic: Literal["general", "news"] | NotGiven = NOT_GIVEN, + | Omit = omit, + days: int | Omit = omit, + exclude_domains: SequenceNotStr[str] | Omit = omit, + include_answer: bool | Omit = omit, + include_domains: SequenceNotStr[str] | Omit = omit, + include_raw_content: Union[Literal["text", "markdown"], bool] | Omit = omit, + max_results: int | Omit = omit, + query: str | Omit = omit, + search_depth: Literal["basic", "advanced"] | Omit = omit, + stream: bool | Omit = omit, + time_range: Literal["day", "week", "month", "year", "d", "w", "m", "y"] | Omit = omit, + topic: Literal["general", "news"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ToolWebSearchResponse: """ Search the web for information about a given query and return relevant results diff --git a/src/writerai/resources/translation.py b/src/writerai/resources/translation.py index 235ce8d8..181c6ab7 100644 --- a/src/writerai/resources/translation.py +++ b/src/writerai/resources/translation.py @@ -7,7 +7,7 @@ import httpx from ..types import translation_translate_params -from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from .._types import Body, Query, Headers, NotGiven, not_given from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -58,7 +58,7 @@ def translate( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> TranslationResponse: """ Translate text from one language to another. @@ -160,7 +160,7 @@ async def translate( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> TranslationResponse: """ Translate text from one language to another. diff --git a/src/writerai/resources/vision.py b/src/writerai/resources/vision.py index 0bea951f..d90bf3d3 100644 --- a/src/writerai/resources/vision.py +++ b/src/writerai/resources/vision.py @@ -8,7 +8,7 @@ import httpx from ..types import vision_analyze_params -from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from .._types import Body, Query, Headers, NotGiven, not_given from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -55,7 +55,7 @@ def analyze( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> VisionResponse: """ Submit images and a prompt to generate an analysis of the images. @@ -123,7 +123,7 @@ async def analyze( extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> VisionResponse: """ Submit images and a prompt to generate an analysis of the images. diff --git a/tests/test_transform.py b/tests/test_transform.py index fe86aaaf..8fb16570 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -8,7 +8,7 @@ import pytest -from writerai._types import NOT_GIVEN, Base64FileInput +from writerai._types import Base64FileInput, omit, not_given from writerai._utils import ( PropertyInfo, transform as _transform, @@ -450,4 +450,11 @@ async def test_transform_skipping(use_async: bool) -> None: @pytest.mark.asyncio async def test_strips_notgiven(use_async: bool) -> None: assert await transform({"foo_bar": "bar"}, Foo1, use_async) == {"fooBar": "bar"} - assert await transform({"foo_bar": NOT_GIVEN}, Foo1, use_async) == {} + assert await transform({"foo_bar": not_given}, Foo1, use_async) == {} + + +@parametrize +@pytest.mark.asyncio +async def test_strips_omit(use_async: bool) -> None: + assert await transform({"foo_bar": "bar"}, Foo1, use_async) == {"fooBar": "bar"} + assert await transform({"foo_bar": omit}, Foo1, use_async) == {} From 22acb8af35c4b0af2214c004afc0372128cba10c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 19 Sep 2025 17:44:39 +0000 Subject: [PATCH 317/399] chore: do not install brew dependencies in ./scripts/bootstrap by default --- scripts/bootstrap | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/scripts/bootstrap b/scripts/bootstrap index e84fe62c..b430fee3 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -4,10 +4,18 @@ set -e cd "$(dirname "$0")/.." -if ! command -v rye >/dev/null 2>&1 && [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ]; then +if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then brew bundle check >/dev/null 2>&1 || { - echo "==> Installing Homebrew dependencies…" - brew bundle + echo -n "==> Install Homebrew dependencies? (y/N): " + read -r response + case "$response" in + [yY][eE][sS]|[yY]) + brew bundle + ;; + *) + ;; + esac + echo } fi From 936a859df70474cfd6b330824d782e47f2788038 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 30 Sep 2025 21:26:42 +0000 Subject: [PATCH 318/399] docs(api): updates to API spec --- .stats.yml | 4 +- src/writerai/types/shared/graph_data.py | 64 +++++++++++++++- .../types/shared_params/graph_data.py | 65 +++++++++++++++- tests/api_resources/test_chat.py | 76 +++++++++++++++++++ 4 files changed, 204 insertions(+), 5 deletions(-) diff --git a/.stats.yml b/.stats.yml index 1822dc87..8b20020e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 33 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-3f87c8deb39e443022f2e04252994a6c9d25473872503edf9eec00d874576b2d.yml -openapi_spec_hash: 5de52bf1d78e00b13a04f6e9ce2f2fb5 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-4ec783072dd7f57c6e021a746df7650fb8d7a164d8ec25c7d5cab06c33bc114f.yml +openapi_spec_hash: ceab065d515f3681b0c33137da308968 config_hash: 7a38bab086b53b43d2a719cb4d883264 diff --git a/src/writerai/types/shared/graph_data.py b/src/writerai/types/shared/graph_data.py index 6898bddd..6d8eb341 100644 --- a/src/writerai/types/shared/graph_data.py +++ b/src/writerai/types/shared/graph_data.py @@ -3,10 +3,66 @@ from typing import List, Optional from typing_extensions import Literal +from pydantic import Field as FieldInfo + from .source import Source from ..._models import BaseModel -__all__ = ["GraphData", "Subquery"] +__all__ = ["GraphData", "References", "ReferencesFile", "ReferencesWeb", "Subquery"] + + +class ReferencesFile(BaseModel): + file_id: str = FieldInfo(alias="fileId") + """The unique identifier of the file in your Writer account.""" + + score: float + """ + Internal score used during the retrieval process for ranking and selecting + relevant snippets. + """ + + text: str + """ + The exact text snippet from the source document that was used to support the + response. + """ + + cite: Optional[str] = None + """ + Unique citation ID that appears in inline citations within the response text + (null if not cited). + """ + + page: Optional[int] = None + """Page number where this snippet was found in the source document.""" + + +class ReferencesWeb(BaseModel): + score: float + """ + Internal score used during the retrieval process for ranking and selecting + relevant snippets. + """ + + text: str + """ + The exact text snippet from the web source that was used to support the + response. + """ + + title: str + """The title of the web page where this content was found.""" + + url: str + """The URL of the web page where this content was found.""" + + +class References(BaseModel): + files: Optional[List[ReferencesFile]] = None + """Array of file-based references from uploaded documents in the Knowledge Graph.""" + + web: Optional[List[ReferencesWeb]] = None + """Array of web-based references from online sources accessed during the query.""" class Subquery(BaseModel): @@ -21,6 +77,12 @@ class Subquery(BaseModel): class GraphData(BaseModel): + references: Optional[References] = None + """ + Detailed source information organized by reference type, providing comprehensive + metadata about the sources used to generate the response. + """ + sources: Optional[List[Optional[Source]]] = None status: Optional[Literal["processing", "finished"]] = None diff --git a/src/writerai/types/shared_params/graph_data.py b/src/writerai/types/shared_params/graph_data.py index caa1e6d5..6e162c1d 100644 --- a/src/writerai/types/shared_params/graph_data.py +++ b/src/writerai/types/shared_params/graph_data.py @@ -3,11 +3,66 @@ from __future__ import annotations from typing import Iterable, Optional -from typing_extensions import Literal, Required, TypedDict +from typing_extensions import Literal, Required, Annotated, TypedDict from .source import Source +from ..._utils import PropertyInfo -__all__ = ["GraphData", "Subquery"] +__all__ = ["GraphData", "References", "ReferencesFile", "ReferencesWeb", "Subquery"] + + +class ReferencesFile(TypedDict, total=False): + file_id: Required[Annotated[str, PropertyInfo(alias="fileId")]] + """The unique identifier of the file in your Writer account.""" + + score: Required[float] + """ + Internal score used during the retrieval process for ranking and selecting + relevant snippets. + """ + + text: Required[str] + """ + The exact text snippet from the source document that was used to support the + response. + """ + + cite: str + """ + Unique citation ID that appears in inline citations within the response text + (null if not cited). + """ + + page: int + """Page number where this snippet was found in the source document.""" + + +class ReferencesWeb(TypedDict, total=False): + score: Required[float] + """ + Internal score used during the retrieval process for ranking and selecting + relevant snippets. + """ + + text: Required[str] + """ + The exact text snippet from the web source that was used to support the + response. + """ + + title: Required[str] + """The title of the web page where this content was found.""" + + url: Required[str] + """The URL of the web page where this content was found.""" + + +class References(TypedDict, total=False): + files: Iterable[ReferencesFile] + """Array of file-based references from uploaded documents in the Knowledge Graph.""" + + web: Iterable[ReferencesWeb] + """Array of web-based references from online sources accessed during the query.""" class Subquery(TypedDict, total=False): @@ -22,6 +77,12 @@ class Subquery(TypedDict, total=False): class GraphData(TypedDict, total=False): + references: References + """ + Detailed source information organized by reference type, providing comprehensive + metadata about the sources used to generate the response. + """ + sources: Iterable[Optional[Source]] status: Optional[Literal["processing", "finished"]] diff --git a/tests/api_resources/test_chat.py b/tests/api_resources/test_chat.py index 8d072394..ab61287b 100644 --- a/tests/api_resources/test_chat.py +++ b/tests/api_resources/test_chat.py @@ -33,6 +33,25 @@ def test_method_chat_with_all_params_overload_1(self, client: Writer) -> None: "role": "user", "content": "string", "graph_data": { + "references": { + "files": [ + { + "file_id": "fileId", + "score": 0, + "text": "text", + "cite": "cite", + "page": 0, + } + ], + "web": [ + { + "score": 0, + "text": "text", + "title": "title", + "url": "https://example.com", + } + ], + }, "sources": [ { "file_id": "file_id", @@ -139,6 +158,25 @@ def test_method_chat_with_all_params_overload_2(self, client: Writer) -> None: "role": "user", "content": "string", "graph_data": { + "references": { + "files": [ + { + "file_id": "fileId", + "score": 0, + "text": "text", + "cite": "cite", + "page": 0, + } + ], + "web": [ + { + "score": 0, + "text": "text", + "title": "title", + "url": "https://example.com", + } + ], + }, "sources": [ { "file_id": "file_id", @@ -251,6 +289,25 @@ async def test_method_chat_with_all_params_overload_1(self, async_client: AsyncW "role": "user", "content": "string", "graph_data": { + "references": { + "files": [ + { + "file_id": "fileId", + "score": 0, + "text": "text", + "cite": "cite", + "page": 0, + } + ], + "web": [ + { + "score": 0, + "text": "text", + "title": "title", + "url": "https://example.com", + } + ], + }, "sources": [ { "file_id": "file_id", @@ -357,6 +414,25 @@ async def test_method_chat_with_all_params_overload_2(self, async_client: AsyncW "role": "user", "content": "string", "graph_data": { + "references": { + "files": [ + { + "file_id": "fileId", + "score": 0, + "text": "text", + "cite": "cite", + "page": 0, + } + ], + "web": [ + { + "score": 0, + "text": "text", + "title": "title", + "url": "https://example.com", + } + ], + }, "sources": [ { "file_id": "file_id", From a263bdae24b7be4c21b451b656f0a20c90cfef29 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 30 Sep 2025 21:31:37 +0000 Subject: [PATCH 319/399] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 8b20020e..8c45d2da 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 33 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-4ec783072dd7f57c6e021a746df7650fb8d7a164d8ec25c7d5cab06c33bc114f.yml openapi_spec_hash: ceab065d515f3681b0c33137da308968 -config_hash: 7a38bab086b53b43d2a719cb4d883264 +config_hash: d655a846f6872554a75412b27b6ed71f From 9e9bd523c48870dee75caba572744ebbadb29666 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 30 Sep 2025 22:00:54 +0000 Subject: [PATCH 320/399] feat(api): manual updates --- .stats.yml | 2 +- src/writerai/resources/tools/comprehend.py | 31 +- src/writerai/resources/tools/tools.py | 113 +++++-- src/writerai/resources/translation.py | 31 +- tests/api_resources/test_tools.py | 328 +++++++++++-------- tests/api_resources/test_translation.py | 138 ++++---- tests/api_resources/tools/test_comprehend.py | 76 +++-- 7 files changed, 429 insertions(+), 290 deletions(-) diff --git a/.stats.yml b/.stats.yml index 8c45d2da..50e9525c 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 33 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-4ec783072dd7f57c6e021a746df7650fb8d7a164d8ec25c7d5cab06c33bc114f.yml openapi_spec_hash: ceab065d515f3681b0c33137da308968 -config_hash: d655a846f6872554a75412b27b6ed71f +config_hash: 2b8696f9cec6810cb2acca7441615269 diff --git a/src/writerai/resources/tools/comprehend.py b/src/writerai/resources/tools/comprehend.py index 07bb2b73..3dc4ac0c 100644 --- a/src/writerai/resources/tools/comprehend.py +++ b/src/writerai/resources/tools/comprehend.py @@ -2,6 +2,7 @@ from __future__ import annotations +import typing_extensions from typing_extensions import Literal import httpx @@ -43,6 +44,9 @@ def with_streaming_response(self) -> ComprehendResourceWithStreamingResponse: """ return ComprehendResourceWithStreamingResponse(self) + @typing_extensions.deprecated( + "Will be removed in a future release. Migrate to `chat.chat` with the LLM tool using the `palmyra-med` model for medical analysis." + ) def medical( self, *, @@ -110,6 +114,9 @@ def with_streaming_response(self) -> AsyncComprehendResourceWithStreamingRespons """ return AsyncComprehendResourceWithStreamingResponse(self) + @typing_extensions.deprecated( + "Will be removed in a future release. Migrate to `chat.chat` with the LLM tool using the `palmyra-med` model for medical analysis." + ) async def medical( self, *, @@ -161,8 +168,10 @@ class ComprehendResourceWithRawResponse: def __init__(self, comprehend: ComprehendResource) -> None: self._comprehend = comprehend - self.medical = to_raw_response_wrapper( - comprehend.medical, + self.medical = ( # pyright: ignore[reportDeprecated] + to_raw_response_wrapper( + comprehend.medical, # pyright: ignore[reportDeprecated], + ) ) @@ -170,8 +179,10 @@ class AsyncComprehendResourceWithRawResponse: def __init__(self, comprehend: AsyncComprehendResource) -> None: self._comprehend = comprehend - self.medical = async_to_raw_response_wrapper( - comprehend.medical, + self.medical = ( # pyright: ignore[reportDeprecated] + async_to_raw_response_wrapper( + comprehend.medical, # pyright: ignore[reportDeprecated], + ) ) @@ -179,8 +190,10 @@ class ComprehendResourceWithStreamingResponse: def __init__(self, comprehend: ComprehendResource) -> None: self._comprehend = comprehend - self.medical = to_streamed_response_wrapper( - comprehend.medical, + self.medical = ( # pyright: ignore[reportDeprecated] + to_streamed_response_wrapper( + comprehend.medical, # pyright: ignore[reportDeprecated], + ) ) @@ -188,6 +201,8 @@ class AsyncComprehendResourceWithStreamingResponse: def __init__(self, comprehend: AsyncComprehendResource) -> None: self._comprehend = comprehend - self.medical = async_to_streamed_response_wrapper( - comprehend.medical, + self.medical = ( # pyright: ignore[reportDeprecated] + async_to_streamed_response_wrapper( + comprehend.medical, # pyright: ignore[reportDeprecated], + ) ) diff --git a/src/writerai/resources/tools/tools.py b/src/writerai/resources/tools/tools.py index 73562f52..47e83ab7 100644 --- a/src/writerai/resources/tools/tools.py +++ b/src/writerai/resources/tools/tools.py @@ -2,6 +2,7 @@ from __future__ import annotations +import typing_extensions from typing import Union from typing_extensions import Literal @@ -64,6 +65,7 @@ def with_streaming_response(self) -> ToolsResourceWithStreamingResponse: """ return ToolsResourceWithStreamingResponse(self) + @typing_extensions.deprecated("Will be removed in a future release. Please migrate to alternative solutions.") def ai_detect( self, *, @@ -101,6 +103,7 @@ def ai_detect( cast_to=ToolAIDetectResponse, ) + @typing_extensions.deprecated("Will be removed in a future release. Please migrate to alternative solutions.") def context_aware_splitting( self, *, @@ -147,6 +150,9 @@ def context_aware_splitting( cast_to=ToolContextAwareSplittingResponse, ) + @typing_extensions.deprecated( + "Will be removed in a future release. A replacement PDF parsing tool for chat completions is planned; see documentation at dev.writer.com for updates." + ) def parse_pdf( self, file_id: str, @@ -184,6 +190,9 @@ def parse_pdf( cast_to=ToolParsePdfResponse, ) + @typing_extensions.deprecated( + "Will be removed in a future release. Migrate to `chat.chat` with the web search tool for web search capabilities." + ) def web_search( self, *, @@ -482,6 +491,7 @@ def with_streaming_response(self) -> AsyncToolsResourceWithStreamingResponse: """ return AsyncToolsResourceWithStreamingResponse(self) + @typing_extensions.deprecated("Will be removed in a future release. Please migrate to alternative solutions.") async def ai_detect( self, *, @@ -519,6 +529,7 @@ async def ai_detect( cast_to=ToolAIDetectResponse, ) + @typing_extensions.deprecated("Will be removed in a future release. Please migrate to alternative solutions.") async def context_aware_splitting( self, *, @@ -565,6 +576,9 @@ async def context_aware_splitting( cast_to=ToolContextAwareSplittingResponse, ) + @typing_extensions.deprecated( + "Will be removed in a future release. A replacement PDF parsing tool for chat completions is planned; see documentation at dev.writer.com for updates." + ) async def parse_pdf( self, file_id: str, @@ -602,6 +616,9 @@ async def parse_pdf( cast_to=ToolParsePdfResponse, ) + @typing_extensions.deprecated( + "Will be removed in a future release. Migrate to `chat.chat` with the web search tool for web search capabilities." + ) async def web_search( self, *, @@ -880,17 +897,25 @@ class ToolsResourceWithRawResponse: def __init__(self, tools: ToolsResource) -> None: self._tools = tools - self.ai_detect = to_raw_response_wrapper( - tools.ai_detect, + self.ai_detect = ( # pyright: ignore[reportDeprecated] + to_raw_response_wrapper( + tools.ai_detect, # pyright: ignore[reportDeprecated], + ) ) - self.context_aware_splitting = to_raw_response_wrapper( - tools.context_aware_splitting, + self.context_aware_splitting = ( # pyright: ignore[reportDeprecated] + to_raw_response_wrapper( + tools.context_aware_splitting, # pyright: ignore[reportDeprecated], + ) ) - self.parse_pdf = to_raw_response_wrapper( - tools.parse_pdf, + self.parse_pdf = ( # pyright: ignore[reportDeprecated] + to_raw_response_wrapper( + tools.parse_pdf, # pyright: ignore[reportDeprecated], + ) ) - self.web_search = to_raw_response_wrapper( - tools.web_search, + self.web_search = ( # pyright: ignore[reportDeprecated] + to_raw_response_wrapper( + tools.web_search, # pyright: ignore[reportDeprecated], + ) ) @cached_property @@ -902,17 +927,25 @@ class AsyncToolsResourceWithRawResponse: def __init__(self, tools: AsyncToolsResource) -> None: self._tools = tools - self.ai_detect = async_to_raw_response_wrapper( - tools.ai_detect, + self.ai_detect = ( # pyright: ignore[reportDeprecated] + async_to_raw_response_wrapper( + tools.ai_detect, # pyright: ignore[reportDeprecated], + ) ) - self.context_aware_splitting = async_to_raw_response_wrapper( - tools.context_aware_splitting, + self.context_aware_splitting = ( # pyright: ignore[reportDeprecated] + async_to_raw_response_wrapper( + tools.context_aware_splitting, # pyright: ignore[reportDeprecated], + ) ) - self.parse_pdf = async_to_raw_response_wrapper( - tools.parse_pdf, + self.parse_pdf = ( # pyright: ignore[reportDeprecated] + async_to_raw_response_wrapper( + tools.parse_pdf, # pyright: ignore[reportDeprecated], + ) ) - self.web_search = async_to_raw_response_wrapper( - tools.web_search, + self.web_search = ( # pyright: ignore[reportDeprecated] + async_to_raw_response_wrapper( + tools.web_search, # pyright: ignore[reportDeprecated], + ) ) @cached_property @@ -924,17 +957,25 @@ class ToolsResourceWithStreamingResponse: def __init__(self, tools: ToolsResource) -> None: self._tools = tools - self.ai_detect = to_streamed_response_wrapper( - tools.ai_detect, + self.ai_detect = ( # pyright: ignore[reportDeprecated] + to_streamed_response_wrapper( + tools.ai_detect, # pyright: ignore[reportDeprecated], + ) ) - self.context_aware_splitting = to_streamed_response_wrapper( - tools.context_aware_splitting, + self.context_aware_splitting = ( # pyright: ignore[reportDeprecated] + to_streamed_response_wrapper( + tools.context_aware_splitting, # pyright: ignore[reportDeprecated], + ) ) - self.parse_pdf = to_streamed_response_wrapper( - tools.parse_pdf, + self.parse_pdf = ( # pyright: ignore[reportDeprecated] + to_streamed_response_wrapper( + tools.parse_pdf, # pyright: ignore[reportDeprecated], + ) ) - self.web_search = to_streamed_response_wrapper( - tools.web_search, + self.web_search = ( # pyright: ignore[reportDeprecated] + to_streamed_response_wrapper( + tools.web_search, # pyright: ignore[reportDeprecated], + ) ) @cached_property @@ -946,17 +987,25 @@ class AsyncToolsResourceWithStreamingResponse: def __init__(self, tools: AsyncToolsResource) -> None: self._tools = tools - self.ai_detect = async_to_streamed_response_wrapper( - tools.ai_detect, + self.ai_detect = ( # pyright: ignore[reportDeprecated] + async_to_streamed_response_wrapper( + tools.ai_detect, # pyright: ignore[reportDeprecated], + ) ) - self.context_aware_splitting = async_to_streamed_response_wrapper( - tools.context_aware_splitting, + self.context_aware_splitting = ( # pyright: ignore[reportDeprecated] + async_to_streamed_response_wrapper( + tools.context_aware_splitting, # pyright: ignore[reportDeprecated], + ) ) - self.parse_pdf = async_to_streamed_response_wrapper( - tools.parse_pdf, + self.parse_pdf = ( # pyright: ignore[reportDeprecated] + async_to_streamed_response_wrapper( + tools.parse_pdf, # pyright: ignore[reportDeprecated], + ) ) - self.web_search = async_to_streamed_response_wrapper( - tools.web_search, + self.web_search = ( # pyright: ignore[reportDeprecated] + async_to_streamed_response_wrapper( + tools.web_search, # pyright: ignore[reportDeprecated], + ) ) @cached_property diff --git a/src/writerai/resources/translation.py b/src/writerai/resources/translation.py index 181c6ab7..da64bf5b 100644 --- a/src/writerai/resources/translation.py +++ b/src/writerai/resources/translation.py @@ -2,6 +2,7 @@ from __future__ import annotations +import typing_extensions from typing_extensions import Literal import httpx @@ -43,6 +44,9 @@ def with_streaming_response(self) -> TranslationResourceWithStreamingResponse: """ return TranslationResourceWithStreamingResponse(self) + @typing_extensions.deprecated( + "Will be removed in a future release. Migrate to `chat.chat` with the translate tool for translation capabilities." + ) def translate( self, *, @@ -145,6 +149,9 @@ def with_streaming_response(self) -> AsyncTranslationResourceWithStreamingRespon """ return AsyncTranslationResourceWithStreamingResponse(self) + @typing_extensions.deprecated( + "Will be removed in a future release. Migrate to `chat.chat` with the translate tool for translation capabilities." + ) async def translate( self, *, @@ -231,8 +238,10 @@ class TranslationResourceWithRawResponse: def __init__(self, translation: TranslationResource) -> None: self._translation = translation - self.translate = to_raw_response_wrapper( - translation.translate, + self.translate = ( # pyright: ignore[reportDeprecated] + to_raw_response_wrapper( + translation.translate, # pyright: ignore[reportDeprecated], + ) ) @@ -240,8 +249,10 @@ class AsyncTranslationResourceWithRawResponse: def __init__(self, translation: AsyncTranslationResource) -> None: self._translation = translation - self.translate = async_to_raw_response_wrapper( - translation.translate, + self.translate = ( # pyright: ignore[reportDeprecated] + async_to_raw_response_wrapper( + translation.translate, # pyright: ignore[reportDeprecated], + ) ) @@ -249,8 +260,10 @@ class TranslationResourceWithStreamingResponse: def __init__(self, translation: TranslationResource) -> None: self._translation = translation - self.translate = to_streamed_response_wrapper( - translation.translate, + self.translate = ( # pyright: ignore[reportDeprecated] + to_streamed_response_wrapper( + translation.translate, # pyright: ignore[reportDeprecated], + ) ) @@ -258,6 +271,8 @@ class AsyncTranslationResourceWithStreamingResponse: def __init__(self, translation: AsyncTranslationResource) -> None: self._translation = translation - self.translate = async_to_streamed_response_wrapper( - translation.translate, + self.translate = ( # pyright: ignore[reportDeprecated] + async_to_streamed_response_wrapper( + translation.translate, # pyright: ignore[reportDeprecated], + ) ) diff --git a/tests/api_resources/test_tools.py b/tests/api_resources/test_tools.py index 8e2787aa..971657ee 100644 --- a/tests/api_resources/test_tools.py +++ b/tests/api_resources/test_tools.py @@ -16,6 +16,8 @@ ToolContextAwareSplittingResponse, ) +# pyright: reportDeprecated=false + base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -24,16 +26,19 @@ class TestTools: @parametrize def test_method_ai_detect(self, client: Writer) -> None: - tool = client.tools.ai_detect( - input="AI and ML continue to be at the forefront of technological advancements. In 2025, we can expect more sophisticated AI systems that can handle complex tasks with greater efficiency. AI will play a crucial role in various sectors, including healthcare, finance, and manufacturing. For instance, AI-powered diagnostic tools will become more accurate, helping doctors detect diseases at an early stage. In finance, AI algorithms will enhance fraud detection and risk management.", - ) + with pytest.warns(DeprecationWarning): + tool = client.tools.ai_detect( + input="AI and ML continue to be at the forefront of technological advancements. In 2025, we can expect more sophisticated AI systems that can handle complex tasks with greater efficiency. AI will play a crucial role in various sectors, including healthcare, finance, and manufacturing. For instance, AI-powered diagnostic tools will become more accurate, helping doctors detect diseases at an early stage. In finance, AI algorithms will enhance fraud detection and risk management.", + ) + assert_matches_type(ToolAIDetectResponse, tool, path=["response"]) @parametrize def test_raw_response_ai_detect(self, client: Writer) -> None: - response = client.tools.with_raw_response.ai_detect( - input="AI and ML continue to be at the forefront of technological advancements. In 2025, we can expect more sophisticated AI systems that can handle complex tasks with greater efficiency. AI will play a crucial role in various sectors, including healthcare, finance, and manufacturing. For instance, AI-powered diagnostic tools will become more accurate, helping doctors detect diseases at an early stage. In finance, AI algorithms will enhance fraud detection and risk management.", - ) + with pytest.warns(DeprecationWarning): + response = client.tools.with_raw_response.ai_detect( + input="AI and ML continue to be at the forefront of technological advancements. In 2025, we can expect more sophisticated AI systems that can handle complex tasks with greater efficiency. AI will play a crucial role in various sectors, including healthcare, finance, and manufacturing. For instance, AI-powered diagnostic tools will become more accurate, helping doctors detect diseases at an early stage. In finance, AI algorithms will enhance fraud detection and risk management.", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -42,31 +47,35 @@ def test_raw_response_ai_detect(self, client: Writer) -> None: @parametrize def test_streaming_response_ai_detect(self, client: Writer) -> None: - with client.tools.with_streaming_response.ai_detect( - input="AI and ML continue to be at the forefront of technological advancements. In 2025, we can expect more sophisticated AI systems that can handle complex tasks with greater efficiency. AI will play a crucial role in various sectors, including healthcare, finance, and manufacturing. For instance, AI-powered diagnostic tools will become more accurate, helping doctors detect diseases at an early stage. In finance, AI algorithms will enhance fraud detection and risk management.", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + with client.tools.with_streaming_response.ai_detect( + input="AI and ML continue to be at the forefront of technological advancements. In 2025, we can expect more sophisticated AI systems that can handle complex tasks with greater efficiency. AI will play a crucial role in various sectors, including healthcare, finance, and manufacturing. For instance, AI-powered diagnostic tools will become more accurate, helping doctors detect diseases at an early stage. In finance, AI algorithms will enhance fraud detection and risk management.", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - tool = response.parse() - assert_matches_type(ToolAIDetectResponse, tool, path=["response"]) + tool = response.parse() + assert_matches_type(ToolAIDetectResponse, tool, path=["response"]) assert cast(Any, response.is_closed) is True @parametrize def test_method_context_aware_splitting(self, client: Writer) -> None: - tool = client.tools.context_aware_splitting( - strategy="llm_split", - text="text", - ) + with pytest.warns(DeprecationWarning): + tool = client.tools.context_aware_splitting( + strategy="llm_split", + text="text", + ) + assert_matches_type(ToolContextAwareSplittingResponse, tool, path=["response"]) @parametrize def test_raw_response_context_aware_splitting(self, client: Writer) -> None: - response = client.tools.with_raw_response.context_aware_splitting( - strategy="llm_split", - text="text", - ) + with pytest.warns(DeprecationWarning): + response = client.tools.with_raw_response.context_aware_splitting( + strategy="llm_split", + text="text", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -75,32 +84,36 @@ def test_raw_response_context_aware_splitting(self, client: Writer) -> None: @parametrize def test_streaming_response_context_aware_splitting(self, client: Writer) -> None: - with client.tools.with_streaming_response.context_aware_splitting( - strategy="llm_split", - text="text", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + with client.tools.with_streaming_response.context_aware_splitting( + strategy="llm_split", + text="text", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - tool = response.parse() - assert_matches_type(ToolContextAwareSplittingResponse, tool, path=["response"]) + tool = response.parse() + assert_matches_type(ToolContextAwareSplittingResponse, tool, path=["response"]) assert cast(Any, response.is_closed) is True @parametrize def test_method_parse_pdf(self, client: Writer) -> None: - tool = client.tools.parse_pdf( - file_id="file_id", - format="text", - ) + with pytest.warns(DeprecationWarning): + tool = client.tools.parse_pdf( + file_id="file_id", + format="text", + ) + assert_matches_type(ToolParsePdfResponse, tool, path=["response"]) @parametrize def test_raw_response_parse_pdf(self, client: Writer) -> None: - response = client.tools.with_raw_response.parse_pdf( - file_id="file_id", - format="text", - ) + with pytest.warns(DeprecationWarning): + response = client.tools.with_raw_response.parse_pdf( + file_id="file_id", + format="text", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -109,53 +122,60 @@ def test_raw_response_parse_pdf(self, client: Writer) -> None: @parametrize def test_streaming_response_parse_pdf(self, client: Writer) -> None: - with client.tools.with_streaming_response.parse_pdf( - file_id="file_id", - format="text", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + with client.tools.with_streaming_response.parse_pdf( + file_id="file_id", + format="text", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - tool = response.parse() - assert_matches_type(ToolParsePdfResponse, tool, path=["response"]) + tool = response.parse() + assert_matches_type(ToolParsePdfResponse, tool, path=["response"]) assert cast(Any, response.is_closed) is True @parametrize def test_path_params_parse_pdf(self, client: Writer) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): - client.tools.with_raw_response.parse_pdf( - file_id="", - format="text", - ) + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): + client.tools.with_raw_response.parse_pdf( + file_id="", + format="text", + ) @parametrize def test_method_web_search(self, client: Writer) -> None: - tool = client.tools.web_search() + with pytest.warns(DeprecationWarning): + tool = client.tools.web_search() + assert_matches_type(ToolWebSearchResponse, tool, path=["response"]) @parametrize def test_method_web_search_with_all_params(self, client: Writer) -> None: - tool = client.tools.web_search( - chunks_per_source=0, - country="afghanistan", - days=0, - exclude_domains=["string"], - include_answer=True, - include_domains=["dev.writer.com"], - include_raw_content="text", - max_results=0, - query="How do I get an API key for the Writer API?", - search_depth="basic", - stream=True, - time_range="day", - topic="general", - ) + with pytest.warns(DeprecationWarning): + tool = client.tools.web_search( + chunks_per_source=0, + country="afghanistan", + days=0, + exclude_domains=["string"], + include_answer=True, + include_domains=["dev.writer.com"], + include_raw_content="text", + max_results=0, + query="How do I get an API key for the Writer API?", + search_depth="basic", + stream=True, + time_range="day", + topic="general", + ) + assert_matches_type(ToolWebSearchResponse, tool, path=["response"]) @parametrize def test_raw_response_web_search(self, client: Writer) -> None: - response = client.tools.with_raw_response.web_search() + with pytest.warns(DeprecationWarning): + response = client.tools.with_raw_response.web_search() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -164,12 +184,13 @@ def test_raw_response_web_search(self, client: Writer) -> None: @parametrize def test_streaming_response_web_search(self, client: Writer) -> None: - with client.tools.with_streaming_response.web_search() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + with client.tools.with_streaming_response.web_search() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - tool = response.parse() - assert_matches_type(ToolWebSearchResponse, tool, path=["response"]) + tool = response.parse() + assert_matches_type(ToolWebSearchResponse, tool, path=["response"]) assert cast(Any, response.is_closed) is True @@ -181,16 +202,19 @@ class TestAsyncTools: @parametrize async def test_method_ai_detect(self, async_client: AsyncWriter) -> None: - tool = await async_client.tools.ai_detect( - input="AI and ML continue to be at the forefront of technological advancements. In 2025, we can expect more sophisticated AI systems that can handle complex tasks with greater efficiency. AI will play a crucial role in various sectors, including healthcare, finance, and manufacturing. For instance, AI-powered diagnostic tools will become more accurate, helping doctors detect diseases at an early stage. In finance, AI algorithms will enhance fraud detection and risk management.", - ) + with pytest.warns(DeprecationWarning): + tool = await async_client.tools.ai_detect( + input="AI and ML continue to be at the forefront of technological advancements. In 2025, we can expect more sophisticated AI systems that can handle complex tasks with greater efficiency. AI will play a crucial role in various sectors, including healthcare, finance, and manufacturing. For instance, AI-powered diagnostic tools will become more accurate, helping doctors detect diseases at an early stage. In finance, AI algorithms will enhance fraud detection and risk management.", + ) + assert_matches_type(ToolAIDetectResponse, tool, path=["response"]) @parametrize async def test_raw_response_ai_detect(self, async_client: AsyncWriter) -> None: - response = await async_client.tools.with_raw_response.ai_detect( - input="AI and ML continue to be at the forefront of technological advancements. In 2025, we can expect more sophisticated AI systems that can handle complex tasks with greater efficiency. AI will play a crucial role in various sectors, including healthcare, finance, and manufacturing. For instance, AI-powered diagnostic tools will become more accurate, helping doctors detect diseases at an early stage. In finance, AI algorithms will enhance fraud detection and risk management.", - ) + with pytest.warns(DeprecationWarning): + response = await async_client.tools.with_raw_response.ai_detect( + input="AI and ML continue to be at the forefront of technological advancements. In 2025, we can expect more sophisticated AI systems that can handle complex tasks with greater efficiency. AI will play a crucial role in various sectors, including healthcare, finance, and manufacturing. For instance, AI-powered diagnostic tools will become more accurate, helping doctors detect diseases at an early stage. In finance, AI algorithms will enhance fraud detection and risk management.", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -199,31 +223,35 @@ async def test_raw_response_ai_detect(self, async_client: AsyncWriter) -> None: @parametrize async def test_streaming_response_ai_detect(self, async_client: AsyncWriter) -> None: - async with async_client.tools.with_streaming_response.ai_detect( - input="AI and ML continue to be at the forefront of technological advancements. In 2025, we can expect more sophisticated AI systems that can handle complex tasks with greater efficiency. AI will play a crucial role in various sectors, including healthcare, finance, and manufacturing. For instance, AI-powered diagnostic tools will become more accurate, helping doctors detect diseases at an early stage. In finance, AI algorithms will enhance fraud detection and risk management.", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + async with async_client.tools.with_streaming_response.ai_detect( + input="AI and ML continue to be at the forefront of technological advancements. In 2025, we can expect more sophisticated AI systems that can handle complex tasks with greater efficiency. AI will play a crucial role in various sectors, including healthcare, finance, and manufacturing. For instance, AI-powered diagnostic tools will become more accurate, helping doctors detect diseases at an early stage. In finance, AI algorithms will enhance fraud detection and risk management.", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - tool = await response.parse() - assert_matches_type(ToolAIDetectResponse, tool, path=["response"]) + tool = await response.parse() + assert_matches_type(ToolAIDetectResponse, tool, path=["response"]) assert cast(Any, response.is_closed) is True @parametrize async def test_method_context_aware_splitting(self, async_client: AsyncWriter) -> None: - tool = await async_client.tools.context_aware_splitting( - strategy="llm_split", - text="text", - ) + with pytest.warns(DeprecationWarning): + tool = await async_client.tools.context_aware_splitting( + strategy="llm_split", + text="text", + ) + assert_matches_type(ToolContextAwareSplittingResponse, tool, path=["response"]) @parametrize async def test_raw_response_context_aware_splitting(self, async_client: AsyncWriter) -> None: - response = await async_client.tools.with_raw_response.context_aware_splitting( - strategy="llm_split", - text="text", - ) + with pytest.warns(DeprecationWarning): + response = await async_client.tools.with_raw_response.context_aware_splitting( + strategy="llm_split", + text="text", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -232,32 +260,36 @@ async def test_raw_response_context_aware_splitting(self, async_client: AsyncWri @parametrize async def test_streaming_response_context_aware_splitting(self, async_client: AsyncWriter) -> None: - async with async_client.tools.with_streaming_response.context_aware_splitting( - strategy="llm_split", - text="text", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + async with async_client.tools.with_streaming_response.context_aware_splitting( + strategy="llm_split", + text="text", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - tool = await response.parse() - assert_matches_type(ToolContextAwareSplittingResponse, tool, path=["response"]) + tool = await response.parse() + assert_matches_type(ToolContextAwareSplittingResponse, tool, path=["response"]) assert cast(Any, response.is_closed) is True @parametrize async def test_method_parse_pdf(self, async_client: AsyncWriter) -> None: - tool = await async_client.tools.parse_pdf( - file_id="file_id", - format="text", - ) + with pytest.warns(DeprecationWarning): + tool = await async_client.tools.parse_pdf( + file_id="file_id", + format="text", + ) + assert_matches_type(ToolParsePdfResponse, tool, path=["response"]) @parametrize async def test_raw_response_parse_pdf(self, async_client: AsyncWriter) -> None: - response = await async_client.tools.with_raw_response.parse_pdf( - file_id="file_id", - format="text", - ) + with pytest.warns(DeprecationWarning): + response = await async_client.tools.with_raw_response.parse_pdf( + file_id="file_id", + format="text", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -266,53 +298,60 @@ async def test_raw_response_parse_pdf(self, async_client: AsyncWriter) -> None: @parametrize async def test_streaming_response_parse_pdf(self, async_client: AsyncWriter) -> None: - async with async_client.tools.with_streaming_response.parse_pdf( - file_id="file_id", - format="text", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + async with async_client.tools.with_streaming_response.parse_pdf( + file_id="file_id", + format="text", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - tool = await response.parse() - assert_matches_type(ToolParsePdfResponse, tool, path=["response"]) + tool = await response.parse() + assert_matches_type(ToolParsePdfResponse, tool, path=["response"]) assert cast(Any, response.is_closed) is True @parametrize async def test_path_params_parse_pdf(self, async_client: AsyncWriter) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): - await async_client.tools.with_raw_response.parse_pdf( - file_id="", - format="text", - ) + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): + await async_client.tools.with_raw_response.parse_pdf( + file_id="", + format="text", + ) @parametrize async def test_method_web_search(self, async_client: AsyncWriter) -> None: - tool = await async_client.tools.web_search() + with pytest.warns(DeprecationWarning): + tool = await async_client.tools.web_search() + assert_matches_type(ToolWebSearchResponse, tool, path=["response"]) @parametrize async def test_method_web_search_with_all_params(self, async_client: AsyncWriter) -> None: - tool = await async_client.tools.web_search( - chunks_per_source=0, - country="afghanistan", - days=0, - exclude_domains=["string"], - include_answer=True, - include_domains=["dev.writer.com"], - include_raw_content="text", - max_results=0, - query="How do I get an API key for the Writer API?", - search_depth="basic", - stream=True, - time_range="day", - topic="general", - ) + with pytest.warns(DeprecationWarning): + tool = await async_client.tools.web_search( + chunks_per_source=0, + country="afghanistan", + days=0, + exclude_domains=["string"], + include_answer=True, + include_domains=["dev.writer.com"], + include_raw_content="text", + max_results=0, + query="How do I get an API key for the Writer API?", + search_depth="basic", + stream=True, + time_range="day", + topic="general", + ) + assert_matches_type(ToolWebSearchResponse, tool, path=["response"]) @parametrize async def test_raw_response_web_search(self, async_client: AsyncWriter) -> None: - response = await async_client.tools.with_raw_response.web_search() + with pytest.warns(DeprecationWarning): + response = await async_client.tools.with_raw_response.web_search() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -321,11 +360,12 @@ async def test_raw_response_web_search(self, async_client: AsyncWriter) -> None: @parametrize async def test_streaming_response_web_search(self, async_client: AsyncWriter) -> None: - async with async_client.tools.with_streaming_response.web_search() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + async with async_client.tools.with_streaming_response.web_search() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - tool = await response.parse() - assert_matches_type(ToolWebSearchResponse, tool, path=["response"]) + tool = await response.parse() + assert_matches_type(ToolWebSearchResponse, tool, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_translation.py b/tests/api_resources/test_translation.py index e7a0babc..12497e7a 100644 --- a/tests/api_resources/test_translation.py +++ b/tests/api_resources/test_translation.py @@ -11,6 +11,8 @@ from tests.utils import assert_matches_type from writerai.types import TranslationResponse +# pyright: reportDeprecated=false + base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -19,28 +21,31 @@ class TestTranslation: @parametrize def test_method_translate(self, client: Writer) -> None: - translation = client.translation.translate( - formality=True, - length_control=True, - mask_profanity=True, - model="palmyra-translate", - source_language_code="en", - target_language_code="es", - text="Hello, world!", - ) + with pytest.warns(DeprecationWarning): + translation = client.translation.translate( + formality=True, + length_control=True, + mask_profanity=True, + model="palmyra-translate", + source_language_code="en", + target_language_code="es", + text="Hello, world!", + ) + assert_matches_type(TranslationResponse, translation, path=["response"]) @parametrize def test_raw_response_translate(self, client: Writer) -> None: - response = client.translation.with_raw_response.translate( - formality=True, - length_control=True, - mask_profanity=True, - model="palmyra-translate", - source_language_code="en", - target_language_code="es", - text="Hello, world!", - ) + with pytest.warns(DeprecationWarning): + response = client.translation.with_raw_response.translate( + formality=True, + length_control=True, + mask_profanity=True, + model="palmyra-translate", + source_language_code="en", + target_language_code="es", + text="Hello, world!", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -49,20 +54,21 @@ def test_raw_response_translate(self, client: Writer) -> None: @parametrize def test_streaming_response_translate(self, client: Writer) -> None: - with client.translation.with_streaming_response.translate( - formality=True, - length_control=True, - mask_profanity=True, - model="palmyra-translate", - source_language_code="en", - target_language_code="es", - text="Hello, world!", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - translation = response.parse() - assert_matches_type(TranslationResponse, translation, path=["response"]) + with pytest.warns(DeprecationWarning): + with client.translation.with_streaming_response.translate( + formality=True, + length_control=True, + mask_profanity=True, + model="palmyra-translate", + source_language_code="en", + target_language_code="es", + text="Hello, world!", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + translation = response.parse() + assert_matches_type(TranslationResponse, translation, path=["response"]) assert cast(Any, response.is_closed) is True @@ -74,28 +80,31 @@ class TestAsyncTranslation: @parametrize async def test_method_translate(self, async_client: AsyncWriter) -> None: - translation = await async_client.translation.translate( - formality=True, - length_control=True, - mask_profanity=True, - model="palmyra-translate", - source_language_code="en", - target_language_code="es", - text="Hello, world!", - ) + with pytest.warns(DeprecationWarning): + translation = await async_client.translation.translate( + formality=True, + length_control=True, + mask_profanity=True, + model="palmyra-translate", + source_language_code="en", + target_language_code="es", + text="Hello, world!", + ) + assert_matches_type(TranslationResponse, translation, path=["response"]) @parametrize async def test_raw_response_translate(self, async_client: AsyncWriter) -> None: - response = await async_client.translation.with_raw_response.translate( - formality=True, - length_control=True, - mask_profanity=True, - model="palmyra-translate", - source_language_code="en", - target_language_code="es", - text="Hello, world!", - ) + with pytest.warns(DeprecationWarning): + response = await async_client.translation.with_raw_response.translate( + formality=True, + length_control=True, + mask_profanity=True, + model="palmyra-translate", + source_language_code="en", + target_language_code="es", + text="Hello, world!", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -104,19 +113,20 @@ async def test_raw_response_translate(self, async_client: AsyncWriter) -> None: @parametrize async def test_streaming_response_translate(self, async_client: AsyncWriter) -> None: - async with async_client.translation.with_streaming_response.translate( - formality=True, - length_control=True, - mask_profanity=True, - model="palmyra-translate", - source_language_code="en", - target_language_code="es", - text="Hello, world!", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - translation = await response.parse() - assert_matches_type(TranslationResponse, translation, path=["response"]) + with pytest.warns(DeprecationWarning): + async with async_client.translation.with_streaming_response.translate( + formality=True, + length_control=True, + mask_profanity=True, + model="palmyra-translate", + source_language_code="en", + target_language_code="es", + text="Hello, world!", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + translation = await response.parse() + assert_matches_type(TranslationResponse, translation, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/tools/test_comprehend.py b/tests/api_resources/tools/test_comprehend.py index 6f7efe39..59e9dd9f 100644 --- a/tests/api_resources/tools/test_comprehend.py +++ b/tests/api_resources/tools/test_comprehend.py @@ -11,6 +11,8 @@ from tests.utils import assert_matches_type from writerai.types.tools import ComprehendMedicalResponse +# pyright: reportDeprecated=false + base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -19,18 +21,21 @@ class TestComprehend: @parametrize def test_method_medical(self, client: Writer) -> None: - comprehend = client.tools.comprehend.medical( - content="content", - response_type="Entities", - ) + with pytest.warns(DeprecationWarning): + comprehend = client.tools.comprehend.medical( + content="content", + response_type="Entities", + ) + assert_matches_type(ComprehendMedicalResponse, comprehend, path=["response"]) @parametrize def test_raw_response_medical(self, client: Writer) -> None: - response = client.tools.comprehend.with_raw_response.medical( - content="content", - response_type="Entities", - ) + with pytest.warns(DeprecationWarning): + response = client.tools.comprehend.with_raw_response.medical( + content="content", + response_type="Entities", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -39,15 +44,16 @@ def test_raw_response_medical(self, client: Writer) -> None: @parametrize def test_streaming_response_medical(self, client: Writer) -> None: - with client.tools.comprehend.with_streaming_response.medical( - content="content", - response_type="Entities", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + with client.tools.comprehend.with_streaming_response.medical( + content="content", + response_type="Entities", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - comprehend = response.parse() - assert_matches_type(ComprehendMedicalResponse, comprehend, path=["response"]) + comprehend = response.parse() + assert_matches_type(ComprehendMedicalResponse, comprehend, path=["response"]) assert cast(Any, response.is_closed) is True @@ -59,18 +65,21 @@ class TestAsyncComprehend: @parametrize async def test_method_medical(self, async_client: AsyncWriter) -> None: - comprehend = await async_client.tools.comprehend.medical( - content="content", - response_type="Entities", - ) + with pytest.warns(DeprecationWarning): + comprehend = await async_client.tools.comprehend.medical( + content="content", + response_type="Entities", + ) + assert_matches_type(ComprehendMedicalResponse, comprehend, path=["response"]) @parametrize async def test_raw_response_medical(self, async_client: AsyncWriter) -> None: - response = await async_client.tools.comprehend.with_raw_response.medical( - content="content", - response_type="Entities", - ) + with pytest.warns(DeprecationWarning): + response = await async_client.tools.comprehend.with_raw_response.medical( + content="content", + response_type="Entities", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -79,14 +88,15 @@ async def test_raw_response_medical(self, async_client: AsyncWriter) -> None: @parametrize async def test_streaming_response_medical(self, async_client: AsyncWriter) -> None: - async with async_client.tools.comprehend.with_streaming_response.medical( - content="content", - response_type="Entities", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - comprehend = await response.parse() - assert_matches_type(ComprehendMedicalResponse, comprehend, path=["response"]) + with pytest.warns(DeprecationWarning): + async with async_client.tools.comprehend.with_streaming_response.medical( + content="content", + response_type="Entities", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + comprehend = await response.parse() + assert_matches_type(ComprehendMedicalResponse, comprehend, path=["response"]) assert cast(Any, response.is_closed) is True From 9c0ac9cb4aa44c60639953fc549be43dd2f35a3b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 30 Sep 2025 22:12:14 +0000 Subject: [PATCH 321/399] feat(api): manual updates --- .stats.yml | 2 +- src/writerai/resources/tools/comprehend.py | 4 ++-- src/writerai/resources/tools/tools.py | 24 ++++++++++++++-------- src/writerai/resources/translation.py | 4 ++-- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/.stats.yml b/.stats.yml index 50e9525c..b3d54722 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 33 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-4ec783072dd7f57c6e021a746df7650fb8d7a164d8ec25c7d5cab06c33bc114f.yml openapi_spec_hash: ceab065d515f3681b0c33137da308968 -config_hash: 2b8696f9cec6810cb2acca7441615269 +config_hash: 089fd5502b9cf91247887b19117f1ca2 diff --git a/src/writerai/resources/tools/comprehend.py b/src/writerai/resources/tools/comprehend.py index 3dc4ac0c..2cff5f4f 100644 --- a/src/writerai/resources/tools/comprehend.py +++ b/src/writerai/resources/tools/comprehend.py @@ -45,7 +45,7 @@ def with_streaming_response(self) -> ComprehendResourceWithStreamingResponse: return ComprehendResourceWithStreamingResponse(self) @typing_extensions.deprecated( - "Will be removed in a future release. Migrate to `chat.chat` with the LLM tool using the `palmyra-med` model for medical analysis." + "Will be removed in a future release. Migrate to `chat.chat` with the LLM tool using the `palmyra-med` model for medical analysis. See documentation at dev.writer.com for more information." ) def medical( self, @@ -115,7 +115,7 @@ def with_streaming_response(self) -> AsyncComprehendResourceWithStreamingRespons return AsyncComprehendResourceWithStreamingResponse(self) @typing_extensions.deprecated( - "Will be removed in a future release. Migrate to `chat.chat` with the LLM tool using the `palmyra-med` model for medical analysis." + "Will be removed in a future release. Migrate to `chat.chat` with the LLM tool using the `palmyra-med` model for medical analysis. See documentation at dev.writer.com for more information." ) async def medical( self, diff --git a/src/writerai/resources/tools/tools.py b/src/writerai/resources/tools/tools.py index 47e83ab7..d48109e0 100644 --- a/src/writerai/resources/tools/tools.py +++ b/src/writerai/resources/tools/tools.py @@ -65,7 +65,9 @@ def with_streaming_response(self) -> ToolsResourceWithStreamingResponse: """ return ToolsResourceWithStreamingResponse(self) - @typing_extensions.deprecated("Will be removed in a future release. Please migrate to alternative solutions.") + @typing_extensions.deprecated( + "Will be removed in a future release. Please migrate to alternative solutions. See documentation at dev.writer.com for more information." + ) def ai_detect( self, *, @@ -103,7 +105,9 @@ def ai_detect( cast_to=ToolAIDetectResponse, ) - @typing_extensions.deprecated("Will be removed in a future release. Please migrate to alternative solutions.") + @typing_extensions.deprecated( + "Will be removed in a future release. Please migrate to alternative solutions. See documentation at dev.writer.com for more information." + ) def context_aware_splitting( self, *, @@ -151,7 +155,7 @@ def context_aware_splitting( ) @typing_extensions.deprecated( - "Will be removed in a future release. A replacement PDF parsing tool for chat completions is planned; see documentation at dev.writer.com for updates." + "Will be removed in a future release. A replacement PDF parsing tool for chat completions is planned; see documentation at dev.writer.com for more information." ) def parse_pdf( self, @@ -191,7 +195,7 @@ def parse_pdf( ) @typing_extensions.deprecated( - "Will be removed in a future release. Migrate to `chat.chat` with the web search tool for web search capabilities." + "Will be removed in a future release. Migrate to `chat.chat` with the web search tool for web search capabilities. See documentation at dev.writer.com for more information." ) def web_search( self, @@ -491,7 +495,9 @@ def with_streaming_response(self) -> AsyncToolsResourceWithStreamingResponse: """ return AsyncToolsResourceWithStreamingResponse(self) - @typing_extensions.deprecated("Will be removed in a future release. Please migrate to alternative solutions.") + @typing_extensions.deprecated( + "Will be removed in a future release. Please migrate to alternative solutions. See documentation at dev.writer.com for more information." + ) async def ai_detect( self, *, @@ -529,7 +535,9 @@ async def ai_detect( cast_to=ToolAIDetectResponse, ) - @typing_extensions.deprecated("Will be removed in a future release. Please migrate to alternative solutions.") + @typing_extensions.deprecated( + "Will be removed in a future release. Please migrate to alternative solutions. See documentation at dev.writer.com for more information." + ) async def context_aware_splitting( self, *, @@ -577,7 +585,7 @@ async def context_aware_splitting( ) @typing_extensions.deprecated( - "Will be removed in a future release. A replacement PDF parsing tool for chat completions is planned; see documentation at dev.writer.com for updates." + "Will be removed in a future release. A replacement PDF parsing tool for chat completions is planned; see documentation at dev.writer.com for more information." ) async def parse_pdf( self, @@ -617,7 +625,7 @@ async def parse_pdf( ) @typing_extensions.deprecated( - "Will be removed in a future release. Migrate to `chat.chat` with the web search tool for web search capabilities." + "Will be removed in a future release. Migrate to `chat.chat` with the web search tool for web search capabilities. See documentation at dev.writer.com for more information." ) async def web_search( self, diff --git a/src/writerai/resources/translation.py b/src/writerai/resources/translation.py index da64bf5b..36166407 100644 --- a/src/writerai/resources/translation.py +++ b/src/writerai/resources/translation.py @@ -45,7 +45,7 @@ def with_streaming_response(self) -> TranslationResourceWithStreamingResponse: return TranslationResourceWithStreamingResponse(self) @typing_extensions.deprecated( - "Will be removed in a future release. Migrate to `chat.chat` with the translate tool for translation capabilities." + "Will be removed in a future release. Migrate to `chat.chat` with the translate tool for translation capabilities. See documentation at dev.writer.com for more information." ) def translate( self, @@ -150,7 +150,7 @@ def with_streaming_response(self) -> AsyncTranslationResourceWithStreamingRespon return AsyncTranslationResourceWithStreamingResponse(self) @typing_extensions.deprecated( - "Will be removed in a future release. Migrate to `chat.chat` with the translate tool for translation capabilities." + "Will be removed in a future release. Migrate to `chat.chat` with the translate tool for translation capabilities. See documentation at dev.writer.com for more information." ) async def translate( self, From a617d5d134962c3237006cb4c1af000a8d1e1548 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 18:52:34 +0000 Subject: [PATCH 322/399] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index f1d45d57..e7a306f4 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.3.2-rc1" + ".": "2.3.2-rc2" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index a1d4f83d..c1d6c88a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "2.3.2-rc1" +version = "2.3.2-rc2" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index 8269c8fb..2241a325 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "2.3.2-rc1" # x-release-please-version +__version__ = "2.3.2-rc2" # x-release-please-version From fc0e5e8232b5d7f5f46c6f6bbf787ccdc6cad18b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 20:21:47 +0000 Subject: [PATCH 323/399] chore(internal): version bump --- .release-please-manifest.json | 2 +- README.md | 4 ++-- pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index e7a306f4..c5e4ca3d 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.3.2-rc2" + ".": "2.3.2" } \ No newline at end of file diff --git a/README.md b/README.md index 8e77ddea..3ecfe7d9 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ The REST API documentation can be found on [dev.writer.com](https://dev.writer.c ```sh # install from PyPI -pip install --pre writer-sdk +pip install writer-sdk ``` ## Usage @@ -89,7 +89,7 @@ You can enable this by installing `aiohttp`: ```sh # install from PyPI -pip install --pre writer-sdk[aiohttp] +pip install writer-sdk[aiohttp] ``` Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: diff --git a/pyproject.toml b/pyproject.toml index c1d6c88a..d8993abc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "2.3.2-rc2" +version = "2.3.2" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index 2241a325..062565c2 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "2.3.2-rc2" # x-release-please-version +__version__ = "2.3.2" # x-release-please-version From 7c4656557cc4a11c6679c5bc54157c00f9803df5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 08:49:20 +0000 Subject: [PATCH 324/399] chore(internal): detect missing future annotations with ruff --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index d8993abc..241e0226 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -224,6 +224,8 @@ select = [ "B", # remove unused imports "F401", + # check for missing future annotations + "FA102", # bare except statements "E722", # unused arguments @@ -246,6 +248,8 @@ unfixable = [ "T203", ] +extend-safe-fixes = ["FA102"] + [tool.ruff.lint.flake8-tidy-imports.banned-api] "functools.lru_cache".msg = "This function does not retain type information for the wrapped function's arguments; The `lru_cache` function from `_utils` should be used instead" From ae5782f4384581e7f204223afe3b4063c10852ed Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 17 Oct 2025 10:21:19 +0000 Subject: [PATCH 325/399] chore: bump `httpx-aiohttp` version to 0.1.9 --- pyproject.toml | 2 +- requirements-dev.lock | 2 +- requirements.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 241e0226..22a03282 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ Homepage = "https://github.com/writer/writer-python" Repository = "https://github.com/writer/writer-python" [project.optional-dependencies] -aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.8"] +aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.9"] [tool.rye] managed = true diff --git a/requirements-dev.lock b/requirements-dev.lock index f024e4cb..b4fda247 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -56,7 +56,7 @@ httpx==0.28.1 # via httpx-aiohttp # via respx # via writer-sdk -httpx-aiohttp==0.1.8 +httpx-aiohttp==0.1.9 # via writer-sdk idna==3.4 # via anyio diff --git a/requirements.lock b/requirements.lock index a26626d9..cb9c5ae2 100644 --- a/requirements.lock +++ b/requirements.lock @@ -43,7 +43,7 @@ httpcore==1.0.9 httpx==0.28.1 # via httpx-aiohttp # via writer-sdk -httpx-aiohttp==0.1.8 +httpx-aiohttp==0.1.9 # via writer-sdk idna==3.4 # via anyio From 8ab91b7f0c8e9dccc9e625a4dc5b2d05d12988e7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 10:44:16 +0000 Subject: [PATCH 326/399] fix(client): close streams without requiring full consumption --- src/writerai/_streaming.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/writerai/_streaming.py b/src/writerai/_streaming.py index ca4711bc..34bb9d92 100644 --- a/src/writerai/_streaming.py +++ b/src/writerai/_streaming.py @@ -76,9 +76,8 @@ def __stream__(self) -> Iterator[_T]: response=self.response, ) - # Ensure the entire stream is consumed - for _sse in iterator: - ... + # As we might not fully consume the response stream, we need to close it explicitly + response.close() def __enter__(self) -> Self: return self @@ -159,9 +158,8 @@ async def __stream__(self) -> AsyncIterator[_T]: response=self.response, ) - # Ensure the entire stream is consumed - async for _sse in iterator: - ... + # As we might not fully consume the response stream, we need to close it explicitly + await response.aclose() async def __aenter__(self) -> Self: return self From 849166933ed385d914a1d338067903d2f7ef0566 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 11:04:11 +0000 Subject: [PATCH 327/399] chore(internal/tests): avoid race condition with implicit client cleanup --- tests/test_client.py | 371 +++++++++++++++++++++++-------------------- 1 file changed, 202 insertions(+), 169 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index c8a764ce..4066755f 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -60,51 +60,49 @@ def _get_open_connections(client: Writer | AsyncWriter) -> int: class TestWriter: - client = Writer(base_url=base_url, api_key=api_key, _strict_response_validation=True) - @pytest.mark.respx(base_url=base_url) - def test_raw_response(self, respx_mock: MockRouter) -> None: + def test_raw_response(self, respx_mock: MockRouter, client: Writer) -> None: respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - response = self.client.post("/foo", cast_to=httpx.Response) + response = client.post("/foo", cast_to=httpx.Response) assert response.status_code == 200 assert isinstance(response, httpx.Response) assert response.json() == {"foo": "bar"} @pytest.mark.respx(base_url=base_url) - def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None: + def test_raw_response_for_binary(self, respx_mock: MockRouter, client: Writer) -> None: respx_mock.post("/foo").mock( return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}') ) - response = self.client.post("/foo", cast_to=httpx.Response) + response = client.post("/foo", cast_to=httpx.Response) assert response.status_code == 200 assert isinstance(response, httpx.Response) assert response.json() == {"foo": "bar"} - def test_copy(self) -> None: - copied = self.client.copy() - assert id(copied) != id(self.client) + def test_copy(self, client: Writer) -> None: + copied = client.copy() + assert id(copied) != id(client) - copied = self.client.copy(api_key="another My API Key") + copied = client.copy(api_key="another My API Key") assert copied.api_key == "another My API Key" - assert self.client.api_key == "My API Key" + assert client.api_key == "My API Key" - def test_copy_default_options(self) -> None: + def test_copy_default_options(self, client: Writer) -> None: # options that have a default are overridden correctly - copied = self.client.copy(max_retries=7) + copied = client.copy(max_retries=7) assert copied.max_retries == 7 - assert self.client.max_retries == 2 + assert client.max_retries == 2 copied2 = copied.copy(max_retries=6) assert copied2.max_retries == 6 assert copied.max_retries == 7 # timeout - assert isinstance(self.client.timeout, httpx.Timeout) - copied = self.client.copy(timeout=None) + assert isinstance(client.timeout, httpx.Timeout) + copied = client.copy(timeout=None) assert copied.timeout is None - assert isinstance(self.client.timeout, httpx.Timeout) + assert isinstance(client.timeout, httpx.Timeout) def test_copy_default_headers(self) -> None: client = Writer( @@ -139,6 +137,7 @@ def test_copy_default_headers(self) -> None: match="`default_headers` and `set_default_headers` arguments are mutually exclusive", ): client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"}) + client.close() def test_copy_default_query(self) -> None: client = Writer( @@ -176,13 +175,15 @@ def test_copy_default_query(self) -> None: ): client.copy(set_default_query={}, default_query={"foo": "Bar"}) - def test_copy_signature(self) -> None: + client.close() + + def test_copy_signature(self, client: Writer) -> None: # ensure the same parameters that can be passed to the client are defined in the `.copy()` method init_signature = inspect.signature( # mypy doesn't like that we access the `__init__` property. - self.client.__init__, # type: ignore[misc] + client.__init__, # type: ignore[misc] ) - copy_signature = inspect.signature(self.client.copy) + copy_signature = inspect.signature(client.copy) exclude_params = {"transport", "proxies", "_strict_response_validation"} for name in init_signature.parameters.keys(): @@ -193,12 +194,12 @@ def test_copy_signature(self) -> None: assert copy_param is not None, f"copy() signature is missing the {name} param" @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12") - def test_copy_build_request(self) -> None: + def test_copy_build_request(self, client: Writer) -> None: options = FinalRequestOptions(method="get", url="/foo") def build_request(options: FinalRequestOptions) -> None: - client = self.client.copy() - client._build_request(options) + client_copy = client.copy() + client_copy._build_request(options) # ensure that the machinery is warmed up before tracing starts. build_request(options) @@ -255,14 +256,12 @@ def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.Statistic print(frame) raise AssertionError() - def test_request_timeout(self) -> None: - request = self.client._build_request(FinalRequestOptions(method="get", url="/foo")) + def test_request_timeout(self, client: Writer) -> None: + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == DEFAULT_TIMEOUT - request = self.client._build_request( - FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0)) - ) + request = client._build_request(FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0))) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == httpx.Timeout(100.0) @@ -273,6 +272,8 @@ def test_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == httpx.Timeout(0) + client.close() + def test_http_client_timeout_option(self) -> None: # custom timeout given to the httpx client should be used with httpx.Client(timeout=None) as http_client: @@ -284,6 +285,8 @@ def test_http_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == httpx.Timeout(None) + client.close() + # no timeout given to the httpx client should not use the httpx default with httpx.Client() as http_client: client = Writer( @@ -294,6 +297,8 @@ def test_http_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == DEFAULT_TIMEOUT + client.close() + # explicitly passing the default timeout currently results in it being ignored with httpx.Client(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: client = Writer( @@ -304,6 +309,8 @@ def test_http_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == DEFAULT_TIMEOUT # our default + client.close() + async def test_invalid_http_client(self) -> None: with pytest.raises(TypeError, match="Invalid `http_client` arg"): async with httpx.AsyncClient() as http_client: @@ -315,14 +322,14 @@ async def test_invalid_http_client(self) -> None: ) def test_default_headers_option(self) -> None: - client = Writer( + test_client = Writer( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + request = test_client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "bar" assert request.headers.get("x-stainless-lang") == "python" - client2 = Writer( + test_client2 = Writer( base_url=base_url, api_key=api_key, _strict_response_validation=True, @@ -331,10 +338,13 @@ def test_default_headers_option(self) -> None: "X-Stainless-Lang": "my-overriding-header", }, ) - request = client2._build_request(FinalRequestOptions(method="get", url="/foo")) + request = test_client2._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "stainless" assert request.headers.get("x-stainless-lang") == "my-overriding-header" + test_client.close() + test_client2.close() + def test_validate_headers(self) -> None: client = Writer(base_url=base_url, api_key=api_key, _strict_response_validation=True) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -363,8 +373,10 @@ def test_default_query_option(self) -> None: url = httpx.URL(request.url) assert dict(url.params) == {"foo": "baz", "query_param": "overridden"} - def test_request_extra_json(self) -> None: - request = self.client._build_request( + client.close() + + def test_request_extra_json(self, client: Writer) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -375,7 +387,7 @@ def test_request_extra_json(self) -> None: data = json.loads(request.content.decode("utf-8")) assert data == {"foo": "bar", "baz": False} - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -386,7 +398,7 @@ def test_request_extra_json(self) -> None: assert data == {"baz": False} # `extra_json` takes priority over `json_data` when keys clash - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -397,8 +409,8 @@ def test_request_extra_json(self) -> None: data = json.loads(request.content.decode("utf-8")) assert data == {"foo": "bar", "baz": None} - def test_request_extra_headers(self) -> None: - request = self.client._build_request( + def test_request_extra_headers(self, client: Writer) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -408,7 +420,7 @@ def test_request_extra_headers(self) -> None: assert request.headers.get("X-Foo") == "Foo" # `extra_headers` takes priority over `default_headers` when keys clash - request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request( + request = client.with_options(default_headers={"X-Bar": "true"})._build_request( FinalRequestOptions( method="post", url="/foo", @@ -419,8 +431,8 @@ def test_request_extra_headers(self) -> None: ) assert request.headers.get("X-Bar") == "false" - def test_request_extra_query(self) -> None: - request = self.client._build_request( + def test_request_extra_query(self, client: Writer) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -433,7 +445,7 @@ def test_request_extra_query(self) -> None: assert params == {"my_query_param": "Foo"} # if both `query` and `extra_query` are given, they are merged - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -447,7 +459,7 @@ def test_request_extra_query(self) -> None: assert params == {"bar": "1", "foo": "2"} # `extra_query` takes priority over `query` when keys clash - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -490,7 +502,7 @@ def test_multipart_repeating_array(self, client: Writer) -> None: ] @pytest.mark.respx(base_url=base_url) - def test_basic_union_response(self, respx_mock: MockRouter) -> None: + def test_basic_union_response(self, respx_mock: MockRouter, client: Writer) -> None: class Model1(BaseModel): name: str @@ -499,12 +511,12 @@ class Model2(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) assert isinstance(response, Model2) assert response.foo == "bar" @pytest.mark.respx(base_url=base_url) - def test_union_response_different_types(self, respx_mock: MockRouter) -> None: + def test_union_response_different_types(self, respx_mock: MockRouter, client: Writer) -> None: """Union of objects with the same field name using a different type""" class Model1(BaseModel): @@ -515,18 +527,18 @@ class Model2(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) assert isinstance(response, Model2) assert response.foo == "bar" respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1})) - response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) assert isinstance(response, Model1) assert response.foo == 1 @pytest.mark.respx(base_url=base_url) - def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None: + def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter, client: Writer) -> None: """ Response that sets Content-Type to something other than application/json but returns json data """ @@ -542,7 +554,7 @@ class Model(BaseModel): ) ) - response = self.client.get("/foo", cast_to=Model) + response = client.get("/foo", cast_to=Model) assert isinstance(response, Model) assert response.foo == 2 @@ -554,6 +566,8 @@ def test_base_url_setter(self) -> None: assert client.base_url == "https://example.com/from_setter/" + client.close() + def test_base_url_env(self) -> None: with update_env(WRITER_BASE_URL="http://localhost:5000/from/env"): client = Writer(api_key=api_key, _strict_response_validation=True) @@ -581,6 +595,7 @@ def test_base_url_trailing_slash(self, client: Writer) -> None: ), ) assert request.url == "http://localhost:5000/custom/path/foo" + client.close() @pytest.mark.parametrize( "client", @@ -604,6 +619,7 @@ def test_base_url_no_trailing_slash(self, client: Writer) -> None: ), ) assert request.url == "http://localhost:5000/custom/path/foo" + client.close() @pytest.mark.parametrize( "client", @@ -627,35 +643,36 @@ def test_absolute_request_url(self, client: Writer) -> None: ), ) assert request.url == "https://myapi.com/foo" + client.close() def test_copied_client_does_not_close_http(self) -> None: - client = Writer(base_url=base_url, api_key=api_key, _strict_response_validation=True) - assert not client.is_closed() + test_client = Writer(base_url=base_url, api_key=api_key, _strict_response_validation=True) + assert not test_client.is_closed() - copied = client.copy() - assert copied is not client + copied = test_client.copy() + assert copied is not test_client del copied - assert not client.is_closed() + assert not test_client.is_closed() def test_client_context_manager(self) -> None: - client = Writer(base_url=base_url, api_key=api_key, _strict_response_validation=True) - with client as c2: - assert c2 is client + test_client = Writer(base_url=base_url, api_key=api_key, _strict_response_validation=True) + with test_client as c2: + assert c2 is test_client assert not c2.is_closed() - assert not client.is_closed() - assert client.is_closed() + assert not test_client.is_closed() + assert test_client.is_closed() @pytest.mark.respx(base_url=base_url) - def test_client_response_validation_error(self, respx_mock: MockRouter) -> None: + def test_client_response_validation_error(self, respx_mock: MockRouter, client: Writer) -> None: class Model(BaseModel): foo: str respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}})) with pytest.raises(APIResponseValidationError) as exc: - self.client.get("/foo", cast_to=Model) + client.get("/foo", cast_to=Model) assert isinstance(exc.value.__cause__, ValidationError) @@ -664,13 +681,13 @@ def test_client_max_retries_validation(self) -> None: Writer(base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None)) @pytest.mark.respx(base_url=base_url) - def test_default_stream_cls(self, respx_mock: MockRouter) -> None: + def test_default_stream_cls(self, respx_mock: MockRouter, client: Writer) -> None: class Model(BaseModel): name: str respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - stream = self.client.post("/foo", cast_to=Model, stream=True, stream_cls=Stream[Model]) + stream = client.post("/foo", cast_to=Model, stream=True, stream_cls=Stream[Model]) assert isinstance(stream, Stream) stream.response.close() @@ -686,11 +703,14 @@ class Model(BaseModel): with pytest.raises(APIResponseValidationError): strict_client.get("/foo", cast_to=Model) - client = Writer(base_url=base_url, api_key=api_key, _strict_response_validation=False) + non_strict_client = Writer(base_url=base_url, api_key=api_key, _strict_response_validation=False) - response = client.get("/foo", cast_to=Model) + response = non_strict_client.get("/foo", cast_to=Model) assert isinstance(response, str) # type: ignore[unreachable] + strict_client.close() + non_strict_client.close() + @pytest.mark.parametrize( "remaining_retries,retry_after,timeout", [ @@ -713,9 +733,9 @@ class Model(BaseModel): ], ) @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) - def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None: - client = Writer(base_url=base_url, api_key=api_key, _strict_response_validation=True) - + def test_parse_retry_after_header( + self, remaining_retries: int, retry_after: str, timeout: float, client: Writer + ) -> None: headers = httpx.Headers({"retry-after": retry_after}) options = FinalRequestOptions(method="get", url="/foo", max_retries=3) calculated = client._calculate_retry_timeout(remaining_retries, options, headers) @@ -729,7 +749,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, clien with pytest.raises(APITimeoutError): client.chat.with_streaming_response.chat(messages=[{"role": "user"}], model="model").__enter__() - assert _get_open_connections(self.client) == 0 + assert _get_open_connections(client) == 0 @mock.patch("writerai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) @@ -738,7 +758,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client with pytest.raises(APIStatusError): client.chat.with_streaming_response.chat(messages=[{"role": "user"}], model="model").__enter__() - assert _get_open_connections(self.client) == 0 + assert _get_open_connections(client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @mock.patch("writerai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @@ -844,83 +864,77 @@ def test_default_client_creation(self) -> None: ) @pytest.mark.respx(base_url=base_url) - def test_follow_redirects(self, respx_mock: MockRouter) -> None: + def test_follow_redirects(self, respx_mock: MockRouter, client: Writer) -> None: # Test that the default follow_redirects=True allows following redirects respx_mock.post("/redirect").mock( return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) ) respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"})) - response = self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) + response = client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) assert response.status_code == 200 assert response.json() == {"status": "ok"} @pytest.mark.respx(base_url=base_url) - def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None: + def test_follow_redirects_disabled(self, respx_mock: MockRouter, client: Writer) -> None: # Test that follow_redirects=False prevents following redirects respx_mock.post("/redirect").mock( return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) ) with pytest.raises(APIStatusError) as exc_info: - self.client.post( - "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response - ) + client.post("/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response) assert exc_info.value.response.status_code == 302 assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected" class TestAsyncWriter: - client = AsyncWriter(base_url=base_url, api_key=api_key, _strict_response_validation=True) - @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio - async def test_raw_response(self, respx_mock: MockRouter) -> None: + async def test_raw_response(self, respx_mock: MockRouter, async_client: AsyncWriter) -> None: respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - response = await self.client.post("/foo", cast_to=httpx.Response) + response = await async_client.post("/foo", cast_to=httpx.Response) assert response.status_code == 200 assert isinstance(response, httpx.Response) assert response.json() == {"foo": "bar"} @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio - async def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None: + async def test_raw_response_for_binary(self, respx_mock: MockRouter, async_client: AsyncWriter) -> None: respx_mock.post("/foo").mock( return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}') ) - response = await self.client.post("/foo", cast_to=httpx.Response) + response = await async_client.post("/foo", cast_to=httpx.Response) assert response.status_code == 200 assert isinstance(response, httpx.Response) assert response.json() == {"foo": "bar"} - def test_copy(self) -> None: - copied = self.client.copy() - assert id(copied) != id(self.client) + def test_copy(self, async_client: AsyncWriter) -> None: + copied = async_client.copy() + assert id(copied) != id(async_client) - copied = self.client.copy(api_key="another My API Key") + copied = async_client.copy(api_key="another My API Key") assert copied.api_key == "another My API Key" - assert self.client.api_key == "My API Key" + assert async_client.api_key == "My API Key" - def test_copy_default_options(self) -> None: + def test_copy_default_options(self, async_client: AsyncWriter) -> None: # options that have a default are overridden correctly - copied = self.client.copy(max_retries=7) + copied = async_client.copy(max_retries=7) assert copied.max_retries == 7 - assert self.client.max_retries == 2 + assert async_client.max_retries == 2 copied2 = copied.copy(max_retries=6) assert copied2.max_retries == 6 assert copied.max_retries == 7 # timeout - assert isinstance(self.client.timeout, httpx.Timeout) - copied = self.client.copy(timeout=None) + assert isinstance(async_client.timeout, httpx.Timeout) + copied = async_client.copy(timeout=None) assert copied.timeout is None - assert isinstance(self.client.timeout, httpx.Timeout) + assert isinstance(async_client.timeout, httpx.Timeout) - def test_copy_default_headers(self) -> None: + async def test_copy_default_headers(self) -> None: client = AsyncWriter( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) @@ -953,8 +967,9 @@ def test_copy_default_headers(self) -> None: match="`default_headers` and `set_default_headers` arguments are mutually exclusive", ): client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"}) + await client.close() - def test_copy_default_query(self) -> None: + async def test_copy_default_query(self) -> None: client = AsyncWriter( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"} ) @@ -990,13 +1005,15 @@ def test_copy_default_query(self) -> None: ): client.copy(set_default_query={}, default_query={"foo": "Bar"}) - def test_copy_signature(self) -> None: + await client.close() + + def test_copy_signature(self, async_client: AsyncWriter) -> None: # ensure the same parameters that can be passed to the client are defined in the `.copy()` method init_signature = inspect.signature( # mypy doesn't like that we access the `__init__` property. - self.client.__init__, # type: ignore[misc] + async_client.__init__, # type: ignore[misc] ) - copy_signature = inspect.signature(self.client.copy) + copy_signature = inspect.signature(async_client.copy) exclude_params = {"transport", "proxies", "_strict_response_validation"} for name in init_signature.parameters.keys(): @@ -1007,12 +1024,12 @@ def test_copy_signature(self) -> None: assert copy_param is not None, f"copy() signature is missing the {name} param" @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12") - def test_copy_build_request(self) -> None: + def test_copy_build_request(self, async_client: AsyncWriter) -> None: options = FinalRequestOptions(method="get", url="/foo") def build_request(options: FinalRequestOptions) -> None: - client = self.client.copy() - client._build_request(options) + client_copy = async_client.copy() + client_copy._build_request(options) # ensure that the machinery is warmed up before tracing starts. build_request(options) @@ -1069,12 +1086,12 @@ def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.Statistic print(frame) raise AssertionError() - async def test_request_timeout(self) -> None: - request = self.client._build_request(FinalRequestOptions(method="get", url="/foo")) + async def test_request_timeout(self, async_client: AsyncWriter) -> None: + request = async_client._build_request(FinalRequestOptions(method="get", url="/foo")) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == DEFAULT_TIMEOUT - request = self.client._build_request( + request = async_client._build_request( FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0)) ) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore @@ -1089,6 +1106,8 @@ async def test_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == httpx.Timeout(0) + await client.close() + async def test_http_client_timeout_option(self) -> None: # custom timeout given to the httpx client should be used async with httpx.AsyncClient(timeout=None) as http_client: @@ -1100,6 +1119,8 @@ async def test_http_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == httpx.Timeout(None) + await client.close() + # no timeout given to the httpx client should not use the httpx default async with httpx.AsyncClient() as http_client: client = AsyncWriter( @@ -1110,6 +1131,8 @@ async def test_http_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == DEFAULT_TIMEOUT + await client.close() + # explicitly passing the default timeout currently results in it being ignored async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: client = AsyncWriter( @@ -1120,6 +1143,8 @@ async def test_http_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == DEFAULT_TIMEOUT # our default + await client.close() + def test_invalid_http_client(self) -> None: with pytest.raises(TypeError, match="Invalid `http_client` arg"): with httpx.Client() as http_client: @@ -1130,15 +1155,15 @@ def test_invalid_http_client(self) -> None: http_client=cast(Any, http_client), ) - def test_default_headers_option(self) -> None: - client = AsyncWriter( + async def test_default_headers_option(self) -> None: + test_client = AsyncWriter( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + request = test_client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "bar" assert request.headers.get("x-stainless-lang") == "python" - client2 = AsyncWriter( + test_client2 = AsyncWriter( base_url=base_url, api_key=api_key, _strict_response_validation=True, @@ -1147,10 +1172,13 @@ def test_default_headers_option(self) -> None: "X-Stainless-Lang": "my-overriding-header", }, ) - request = client2._build_request(FinalRequestOptions(method="get", url="/foo")) + request = test_client2._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "stainless" assert request.headers.get("x-stainless-lang") == "my-overriding-header" + await test_client.close() + await test_client2.close() + def test_validate_headers(self) -> None: client = AsyncWriter(base_url=base_url, api_key=api_key, _strict_response_validation=True) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -1161,7 +1189,7 @@ def test_validate_headers(self) -> None: client2 = AsyncWriter(base_url=base_url, api_key=None, _strict_response_validation=True) _ = client2 - def test_default_query_option(self) -> None: + async def test_default_query_option(self) -> None: client = AsyncWriter( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} ) @@ -1179,8 +1207,10 @@ def test_default_query_option(self) -> None: url = httpx.URL(request.url) assert dict(url.params) == {"foo": "baz", "query_param": "overridden"} - def test_request_extra_json(self) -> None: - request = self.client._build_request( + await client.close() + + def test_request_extra_json(self, client: Writer) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1191,7 +1221,7 @@ def test_request_extra_json(self) -> None: data = json.loads(request.content.decode("utf-8")) assert data == {"foo": "bar", "baz": False} - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1202,7 +1232,7 @@ def test_request_extra_json(self) -> None: assert data == {"baz": False} # `extra_json` takes priority over `json_data` when keys clash - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1213,8 +1243,8 @@ def test_request_extra_json(self) -> None: data = json.loads(request.content.decode("utf-8")) assert data == {"foo": "bar", "baz": None} - def test_request_extra_headers(self) -> None: - request = self.client._build_request( + def test_request_extra_headers(self, client: Writer) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1224,7 +1254,7 @@ def test_request_extra_headers(self) -> None: assert request.headers.get("X-Foo") == "Foo" # `extra_headers` takes priority over `default_headers` when keys clash - request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request( + request = client.with_options(default_headers={"X-Bar": "true"})._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1235,8 +1265,8 @@ def test_request_extra_headers(self) -> None: ) assert request.headers.get("X-Bar") == "false" - def test_request_extra_query(self) -> None: - request = self.client._build_request( + def test_request_extra_query(self, client: Writer) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1249,7 +1279,7 @@ def test_request_extra_query(self) -> None: assert params == {"my_query_param": "Foo"} # if both `query` and `extra_query` are given, they are merged - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1263,7 +1293,7 @@ def test_request_extra_query(self) -> None: assert params == {"bar": "1", "foo": "2"} # `extra_query` takes priority over `query` when keys clash - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1306,7 +1336,7 @@ def test_multipart_repeating_array(self, async_client: AsyncWriter) -> None: ] @pytest.mark.respx(base_url=base_url) - async def test_basic_union_response(self, respx_mock: MockRouter) -> None: + async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncWriter) -> None: class Model1(BaseModel): name: str @@ -1315,12 +1345,12 @@ class Model2(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) assert isinstance(response, Model2) assert response.foo == "bar" @pytest.mark.respx(base_url=base_url) - async def test_union_response_different_types(self, respx_mock: MockRouter) -> None: + async def test_union_response_different_types(self, respx_mock: MockRouter, async_client: AsyncWriter) -> None: """Union of objects with the same field name using a different type""" class Model1(BaseModel): @@ -1331,18 +1361,20 @@ class Model2(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) assert isinstance(response, Model2) assert response.foo == "bar" respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1})) - response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) assert isinstance(response, Model1) assert response.foo == 1 @pytest.mark.respx(base_url=base_url) - async def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None: + async def test_non_application_json_content_type_for_json_data( + self, respx_mock: MockRouter, async_client: AsyncWriter + ) -> None: """ Response that sets Content-Type to something other than application/json but returns json data """ @@ -1358,11 +1390,11 @@ class Model(BaseModel): ) ) - response = await self.client.get("/foo", cast_to=Model) + response = await async_client.get("/foo", cast_to=Model) assert isinstance(response, Model) assert response.foo == 2 - def test_base_url_setter(self) -> None: + async def test_base_url_setter(self) -> None: client = AsyncWriter( base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True ) @@ -1372,7 +1404,9 @@ def test_base_url_setter(self) -> None: assert client.base_url == "https://example.com/from_setter/" - def test_base_url_env(self) -> None: + await client.close() + + async def test_base_url_env(self) -> None: with update_env(WRITER_BASE_URL="http://localhost:5000/from/env"): client = AsyncWriter(api_key=api_key, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" @@ -1392,7 +1426,7 @@ def test_base_url_env(self) -> None: ], ids=["standard", "custom http client"], ) - def test_base_url_trailing_slash(self, client: AsyncWriter) -> None: + async def test_base_url_trailing_slash(self, client: AsyncWriter) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -1401,6 +1435,7 @@ def test_base_url_trailing_slash(self, client: AsyncWriter) -> None: ), ) assert request.url == "http://localhost:5000/custom/path/foo" + await client.close() @pytest.mark.parametrize( "client", @@ -1417,7 +1452,7 @@ def test_base_url_trailing_slash(self, client: AsyncWriter) -> None: ], ids=["standard", "custom http client"], ) - def test_base_url_no_trailing_slash(self, client: AsyncWriter) -> None: + async def test_base_url_no_trailing_slash(self, client: AsyncWriter) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -1426,6 +1461,7 @@ def test_base_url_no_trailing_slash(self, client: AsyncWriter) -> None: ), ) assert request.url == "http://localhost:5000/custom/path/foo" + await client.close() @pytest.mark.parametrize( "client", @@ -1442,7 +1478,7 @@ def test_base_url_no_trailing_slash(self, client: AsyncWriter) -> None: ], ids=["standard", "custom http client"], ) - def test_absolute_request_url(self, client: AsyncWriter) -> None: + async def test_absolute_request_url(self, client: AsyncWriter) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -1451,37 +1487,37 @@ def test_absolute_request_url(self, client: AsyncWriter) -> None: ), ) assert request.url == "https://myapi.com/foo" + await client.close() async def test_copied_client_does_not_close_http(self) -> None: - client = AsyncWriter(base_url=base_url, api_key=api_key, _strict_response_validation=True) - assert not client.is_closed() + test_client = AsyncWriter(base_url=base_url, api_key=api_key, _strict_response_validation=True) + assert not test_client.is_closed() - copied = client.copy() - assert copied is not client + copied = test_client.copy() + assert copied is not test_client del copied await asyncio.sleep(0.2) - assert not client.is_closed() + assert not test_client.is_closed() async def test_client_context_manager(self) -> None: - client = AsyncWriter(base_url=base_url, api_key=api_key, _strict_response_validation=True) - async with client as c2: - assert c2 is client + test_client = AsyncWriter(base_url=base_url, api_key=api_key, _strict_response_validation=True) + async with test_client as c2: + assert c2 is test_client assert not c2.is_closed() - assert not client.is_closed() - assert client.is_closed() + assert not test_client.is_closed() + assert test_client.is_closed() @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio - async def test_client_response_validation_error(self, respx_mock: MockRouter) -> None: + async def test_client_response_validation_error(self, respx_mock: MockRouter, async_client: AsyncWriter) -> None: class Model(BaseModel): foo: str respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}})) with pytest.raises(APIResponseValidationError) as exc: - await self.client.get("/foo", cast_to=Model) + await async_client.get("/foo", cast_to=Model) assert isinstance(exc.value.__cause__, ValidationError) @@ -1492,19 +1528,17 @@ async def test_client_max_retries_validation(self) -> None: ) @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio - async def test_default_stream_cls(self, respx_mock: MockRouter) -> None: + async def test_default_stream_cls(self, respx_mock: MockRouter, async_client: AsyncWriter) -> None: class Model(BaseModel): name: str respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - stream = await self.client.post("/foo", cast_to=Model, stream=True, stream_cls=AsyncStream[Model]) + stream = await async_client.post("/foo", cast_to=Model, stream=True, stream_cls=AsyncStream[Model]) assert isinstance(stream, AsyncStream) await stream.response.aclose() @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio async def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None: class Model(BaseModel): name: str @@ -1516,11 +1550,14 @@ class Model(BaseModel): with pytest.raises(APIResponseValidationError): await strict_client.get("/foo", cast_to=Model) - client = AsyncWriter(base_url=base_url, api_key=api_key, _strict_response_validation=False) + non_strict_client = AsyncWriter(base_url=base_url, api_key=api_key, _strict_response_validation=False) - response = await client.get("/foo", cast_to=Model) + response = await non_strict_client.get("/foo", cast_to=Model) assert isinstance(response, str) # type: ignore[unreachable] + await strict_client.close() + await non_strict_client.close() + @pytest.mark.parametrize( "remaining_retries,retry_after,timeout", [ @@ -1543,13 +1580,12 @@ class Model(BaseModel): ], ) @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) - @pytest.mark.asyncio - async def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None: - client = AsyncWriter(base_url=base_url, api_key=api_key, _strict_response_validation=True) - + async def test_parse_retry_after_header( + self, remaining_retries: int, retry_after: str, timeout: float, async_client: AsyncWriter + ) -> None: headers = httpx.Headers({"retry-after": retry_after}) options = FinalRequestOptions(method="get", url="/foo", max_retries=3) - calculated = client._calculate_retry_timeout(remaining_retries, options, headers) + calculated = async_client._calculate_retry_timeout(remaining_retries, options, headers) assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType] @mock.patch("writerai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @@ -1562,7 +1598,7 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, messages=[{"role": "user"}], model="model" ).__aenter__() - assert _get_open_connections(self.client) == 0 + assert _get_open_connections(async_client) == 0 @mock.patch("writerai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) @@ -1573,12 +1609,11 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, await async_client.chat.with_streaming_response.chat( messages=[{"role": "user"}], model="model" ).__aenter__() - assert _get_open_connections(self.client) == 0 + assert _get_open_connections(async_client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @mock.patch("writerai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio @pytest.mark.parametrize("failure_mode", ["status", "exception"]) async def test_retries_taken( self, @@ -1610,7 +1645,6 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @mock.patch("writerai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio async def test_omit_retry_count_header( self, async_client: AsyncWriter, failures_before_success: int, respx_mock: MockRouter ) -> None: @@ -1636,7 +1670,6 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @mock.patch("writerai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio async def test_overwrite_retry_count_header( self, async_client: AsyncWriter, failures_before_success: int, respx_mock: MockRouter ) -> None: @@ -1686,26 +1719,26 @@ async def test_default_client_creation(self) -> None: ) @pytest.mark.respx(base_url=base_url) - async def test_follow_redirects(self, respx_mock: MockRouter) -> None: + async def test_follow_redirects(self, respx_mock: MockRouter, async_client: AsyncWriter) -> None: # Test that the default follow_redirects=True allows following redirects respx_mock.post("/redirect").mock( return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) ) respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"})) - response = await self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) + response = await async_client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) assert response.status_code == 200 assert response.json() == {"status": "ok"} @pytest.mark.respx(base_url=base_url) - async def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None: + async def test_follow_redirects_disabled(self, respx_mock: MockRouter, async_client: AsyncWriter) -> None: # Test that follow_redirects=False prevents following redirects respx_mock.post("/redirect").mock( return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) ) with pytest.raises(APIStatusError) as exc_info: - await self.client.post( + await async_client.post( "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response ) From bb3f8bd5002827091f12b68909b62b3b82bdd223 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 16:11:44 +0000 Subject: [PATCH 328/399] chore(internal): grammar fix (it's -> its) --- src/writerai/_utils/_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/writerai/_utils/_utils.py b/src/writerai/_utils/_utils.py index 50d59269..eec7f4a1 100644 --- a/src/writerai/_utils/_utils.py +++ b/src/writerai/_utils/_utils.py @@ -133,7 +133,7 @@ def is_given(obj: _T | NotGiven | Omit) -> TypeGuard[_T]: # Type safe methods for narrowing types with TypeVars. # The default narrowing for isinstance(obj, dict) is dict[unknown, unknown], # however this cause Pyright to rightfully report errors. As we know we don't -# care about the contained types we can safely use `object` in it's place. +# care about the contained types we can safely use `object` in its place. # # There are two separate functions defined, `is_*` and `is_*_t` for different use cases. # `is_*` is for when you're dealing with an unknown input From 26818954c3604720cbd37ab877a1829128e0e362 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 11:30:28 +0000 Subject: [PATCH 329/399] chore(package): drop Python 3.8 support --- README.md | 4 ++-- pyproject.toml | 5 ++--- src/writerai/_utils/_sync.py | 34 +++------------------------------- 3 files changed, 7 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 3ecfe7d9..2619cb28 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![PyPI version](https://img.shields.io/pypi/v/writer-sdk.svg?label=pypi%20(stable))](https://pypi.org/project/writer-sdk/) -The Writer Python library provides convenient access to the Writer REST API from any Python 3.8+ +The Writer Python library provides convenient access to the Writer 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). @@ -542,7 +542,7 @@ print(writerai.__version__) ## Requirements -Python 3.8 or higher. +Python 3.9 or higher. ## Contributing diff --git a/pyproject.toml b/pyproject.toml index 22a03282..f99fbc2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,11 +15,10 @@ dependencies = [ "distro>=1.7.0, <2", "sniffio", ] -requires-python = ">= 3.8" +requires-python = ">= 3.9" classifiers = [ "Typing :: Typed", "Intended Audience :: Developers", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -141,7 +140,7 @@ filterwarnings = [ # there are a couple of flags that are still disabled by # default in strict mode as they are experimental and niche. typeCheckingMode = "strict" -pythonVersion = "3.8" +pythonVersion = "3.9" exclude = [ "_dev", diff --git a/src/writerai/_utils/_sync.py b/src/writerai/_utils/_sync.py index ad7ec71b..f6027c18 100644 --- a/src/writerai/_utils/_sync.py +++ b/src/writerai/_utils/_sync.py @@ -1,10 +1,8 @@ from __future__ import annotations -import sys import asyncio import functools -import contextvars -from typing import Any, TypeVar, Callable, Awaitable +from typing import TypeVar, Callable, Awaitable from typing_extensions import ParamSpec import anyio @@ -15,34 +13,11 @@ T_ParamSpec = ParamSpec("T_ParamSpec") -if sys.version_info >= (3, 9): - _asyncio_to_thread = asyncio.to_thread -else: - # backport of https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread - # for Python 3.8 support - async def _asyncio_to_thread( - func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs - ) -> Any: - """Asynchronously run function *func* in a separate thread. - - Any *args and **kwargs supplied for this function are directly passed - to *func*. Also, the current :class:`contextvars.Context` is propagated, - allowing context variables from the main thread to be accessed in the - separate thread. - - Returns a coroutine that can be awaited to get the eventual result of *func*. - """ - loop = asyncio.events.get_running_loop() - ctx = contextvars.copy_context() - func_call = functools.partial(ctx.run, func, *args, **kwargs) - return await loop.run_in_executor(None, func_call) - - async def to_thread( func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs ) -> T_Retval: if sniffio.current_async_library() == "asyncio": - return await _asyncio_to_thread(func, *args, **kwargs) + return await asyncio.to_thread(func, *args, **kwargs) return await anyio.to_thread.run_sync( functools.partial(func, *args, **kwargs), @@ -53,10 +28,7 @@ async def to_thread( def asyncify(function: Callable[T_ParamSpec, T_Retval]) -> Callable[T_ParamSpec, Awaitable[T_Retval]]: """ Take a blocking function and create an async one that receives the same - positional and keyword arguments. For python version 3.9 and above, it uses - asyncio.to_thread to run the function in a separate thread. For python version - 3.8, it uses locally defined copy of the asyncio.to_thread function which was - introduced in python 3.9. + positional and keyword arguments. Usage: From 317a3ec524bde545b01a456e5aedf5ced0e237f8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 13:37:33 +0000 Subject: [PATCH 330/399] fix: compat with Python 3.14 --- src/writerai/_models.py | 11 ++++++++--- tests/test_models.py | 8 ++++---- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/writerai/_models.py b/src/writerai/_models.py index 6a3cd1d2..fcec2cf9 100644 --- a/src/writerai/_models.py +++ b/src/writerai/_models.py @@ -2,6 +2,7 @@ import os import inspect +import weakref from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast from datetime import date, datetime from typing_extensions import ( @@ -573,6 +574,9 @@ class CachedDiscriminatorType(Protocol): __discriminator__: DiscriminatorDetails +DISCRIMINATOR_CACHE: weakref.WeakKeyDictionary[type, DiscriminatorDetails] = weakref.WeakKeyDictionary() + + class DiscriminatorDetails: field_name: str """The name of the discriminator field in the variant class, e.g. @@ -615,8 +619,9 @@ def __init__( def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, ...]) -> DiscriminatorDetails | None: - if isinstance(union, CachedDiscriminatorType): - return union.__discriminator__ + cached = DISCRIMINATOR_CACHE.get(union) + if cached is not None: + return cached discriminator_field_name: str | None = None @@ -669,7 +674,7 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, discriminator_field=discriminator_field_name, discriminator_alias=discriminator_alias, ) - cast(CachedDiscriminatorType, union).__discriminator__ = details + DISCRIMINATOR_CACHE.setdefault(union, details) return details diff --git a/tests/test_models.py b/tests/test_models.py index af9b6e48..d5169d03 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -9,7 +9,7 @@ from writerai._utils import PropertyInfo from writerai._compat import PYDANTIC_V1, parse_obj, model_dump, model_json -from writerai._models import BaseModel, construct_type +from writerai._models import DISCRIMINATOR_CACHE, BaseModel, construct_type class BasicModel(BaseModel): @@ -809,7 +809,7 @@ class B(BaseModel): UnionType = cast(Any, Union[A, B]) - assert not hasattr(UnionType, "__discriminator__") + assert not DISCRIMINATOR_CACHE.get(UnionType) m = construct_type( value={"type": "b", "data": "foo"}, type_=cast(Any, Annotated[UnionType, PropertyInfo(discriminator="type")]) @@ -818,7 +818,7 @@ class B(BaseModel): assert m.type == "b" assert m.data == "foo" # type: ignore[comparison-overlap] - discriminator = UnionType.__discriminator__ + discriminator = DISCRIMINATOR_CACHE.get(UnionType) assert discriminator is not None m = construct_type( @@ -830,7 +830,7 @@ class B(BaseModel): # if the discriminator details object stays the same between invocations then # we hit the cache - assert UnionType.__discriminator__ is discriminator + assert DISCRIMINATOR_CACHE.get(UnionType) is discriminator @pytest.mark.skipif(PYDANTIC_V1, reason="TypeAliasType is not supported in Pydantic v1") From e84a121d7278b6eb45880b2c51aa7001cfe1d7a2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 18:51:28 +0000 Subject: [PATCH 331/399] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index b3d54722..9ff96150 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 33 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-4ec783072dd7f57c6e021a746df7650fb8d7a164d8ec25c7d5cab06c33bc114f.yml openapi_spec_hash: ceab065d515f3681b0c33137da308968 -config_hash: 089fd5502b9cf91247887b19117f1ca2 +config_hash: 886645f89dc98f04b8931eaf02854e5f From d46118e07aea4a86fde5c7afcb2ff5e9faea9f10 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 11 Nov 2025 14:37:29 +0000 Subject: [PATCH 332/399] fix(compat): update signatures of `model_dump` and `model_dump_json` for Pydantic v1 --- src/writerai/_models.py | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/src/writerai/_models.py b/src/writerai/_models.py index fcec2cf9..ca9500b2 100644 --- a/src/writerai/_models.py +++ b/src/writerai/_models.py @@ -257,15 +257,16 @@ def model_dump( mode: Literal["json", "python"] | str = "python", include: IncEx | None = None, exclude: IncEx | None = None, + context: Any | None = None, by_alias: bool | None = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, + exclude_computed_fields: bool = False, round_trip: bool = False, warnings: bool | Literal["none", "warn", "error"] = True, - context: dict[str, Any] | None = None, - serialize_as_any: bool = False, fallback: Callable[[Any], Any] | None = None, + serialize_as_any: bool = False, ) -> dict[str, Any]: """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump @@ -273,16 +274,24 @@ def model_dump( Args: mode: The mode in which `to_python` should run. - If mode is 'json', the dictionary will only contain JSON serializable types. - If mode is 'python', the dictionary may contain any Python objects. - include: A list of fields to include in the output. - exclude: A list of fields to exclude from the output. + If mode is 'json', the output will only contain JSON serializable types. + If mode is 'python', the output may contain non-JSON-serializable Python objects. + include: A set of fields to include in the output. + exclude: A set of fields to exclude from the output. + context: Additional context to pass to the serializer. by_alias: Whether to use the field's alias in the dictionary key if defined. - exclude_unset: Whether to exclude fields that are unset or None from the output. - exclude_defaults: Whether to exclude fields that are set to their default value from the output. - exclude_none: Whether to exclude fields that have a value of `None` from the output. - round_trip: Whether to enable serialization and deserialization round-trip support. - warnings: Whether to log warnings when invalid fields are encountered. + exclude_unset: Whether to exclude fields that have not been explicitly set. + exclude_defaults: Whether to exclude fields that are set to their default value. + exclude_none: Whether to exclude fields that have a value of `None`. + exclude_computed_fields: Whether to exclude computed fields. + While this can be useful for round-tripping, it is usually recommended to use the dedicated + `round_trip` parameter instead. + round_trip: If True, dumped values should be valid as input for non-idempotent types such as Json[T]. + warnings: How to handle serialization errors. False/"none" ignores them, True/"warn" logs errors, + "error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError]. + fallback: A function to call when an unknown value is encountered. If not provided, + a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised. + serialize_as_any: Whether to serialize fields with duck-typing serialization behavior. Returns: A dictionary representation of the model. @@ -299,6 +308,8 @@ def model_dump( raise ValueError("serialize_as_any is only supported in Pydantic v2") if fallback is not None: raise ValueError("fallback is only supported in Pydantic v2") + if exclude_computed_fields != False: + raise ValueError("exclude_computed_fields is only supported in Pydantic v2") dumped = super().dict( # pyright: ignore[reportDeprecated] include=include, exclude=exclude, @@ -315,15 +326,17 @@ def model_dump_json( self, *, indent: int | None = None, + ensure_ascii: bool = False, include: IncEx | None = None, exclude: IncEx | None = None, + context: Any | None = None, by_alias: bool | None = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, + exclude_computed_fields: bool = False, round_trip: bool = False, warnings: bool | Literal["none", "warn", "error"] = True, - context: dict[str, Any] | None = None, fallback: Callable[[Any], Any] | None = None, serialize_as_any: bool = False, ) -> str: @@ -355,6 +368,10 @@ def model_dump_json( raise ValueError("serialize_as_any is only supported in Pydantic v2") if fallback is not None: raise ValueError("fallback is only supported in Pydantic v2") + if ensure_ascii != False: + raise ValueError("ensure_ascii is only supported in Pydantic v2") + if exclude_computed_fields != False: + raise ValueError("exclude_computed_fields is only supported in Pydantic v2") return super().json( # type: ignore[reportDeprecated] indent=indent, include=include, From d66536729e7f9be03f14338d0e8f61242dee0ca2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 21 Nov 2025 23:28:44 +0000 Subject: [PATCH 333/399] chore(internal): codegen related update --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index f99fbc2d..505b90d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: MacOS", From 113a09771a983ea5243203f682ea7fadf2dad152 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 16:52:14 +0000 Subject: [PATCH 334/399] fix: ensure streams are always closed --- src/writerai/_streaming.py | 98 +++++++++++++++++++------------------- 1 file changed, 50 insertions(+), 48 deletions(-) diff --git a/src/writerai/_streaming.py b/src/writerai/_streaming.py index 34bb9d92..a9e69241 100644 --- a/src/writerai/_streaming.py +++ b/src/writerai/_streaming.py @@ -54,30 +54,31 @@ def __stream__(self) -> Iterator[_T]: process_data = self._client._process_response_data iterator = self._iter_events() - for sse in iterator: - if sse.data.startswith("[DONE]"): - break - - if sse.event is None: - yield process_data(data=sse.json(), cast_to=cast_to, response=response) - - if sse.event == "error": - body = sse.data - - try: - body = sse.json() - err_msg = f"{body}" - except Exception: - err_msg = sse.data or f"Error code: {response.status_code}" - - raise self._client._make_status_error( - err_msg, - body=body, - response=self.response, - ) - - # As we might not fully consume the response stream, we need to close it explicitly - response.close() + try: + for sse in iterator: + if sse.data.startswith("[DONE]"): + break + + if sse.event is None: + yield process_data(data=sse.json(), cast_to=cast_to, response=response) + + if sse.event == "error": + body = sse.data + + try: + body = sse.json() + err_msg = f"{body}" + except Exception: + err_msg = sse.data or f"Error code: {response.status_code}" + + raise self._client._make_status_error( + err_msg, + body=body, + response=self.response, + ) + finally: + # Ensure the response is closed even if the consumer doesn't read all data + response.close() def __enter__(self) -> Self: return self @@ -136,30 +137,31 @@ async def __stream__(self) -> AsyncIterator[_T]: process_data = self._client._process_response_data iterator = self._iter_events() - async for sse in iterator: - if sse.data.startswith("[DONE]"): - break - - if sse.event is None: - yield process_data(data=sse.json(), cast_to=cast_to, response=response) - - if sse.event == "error": - body = sse.data - - try: - body = sse.json() - err_msg = f"{body}" - except Exception: - err_msg = sse.data or f"Error code: {response.status_code}" - - raise self._client._make_status_error( - err_msg, - body=body, - response=self.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: + if sse.data.startswith("[DONE]"): + break + + if sse.event is None: + yield process_data(data=sse.json(), cast_to=cast_to, response=response) + + if sse.event == "error": + body = sse.data + + try: + body = sse.json() + err_msg = f"{body}" + except Exception: + err_msg = sse.data or f"Error code: {response.status_code}" + + raise self._client._make_status_error( + err_msg, + body=body, + response=self.response, + ) + finally: + # Ensure the response is closed even if the consumer doesn't read all data + await response.aclose() async def __aenter__(self) -> Self: return self From 78f1ae274e05158f396cf08dab42d1f14d645b03 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 18:49:14 +0000 Subject: [PATCH 335/399] chore(deps): mypy 1.18.1 has a regression, pin to 1.17 --- pyproject.toml | 2 +- requirements-dev.lock | 4 +++- requirements.lock | 8 ++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 505b90d1..093bd415 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ managed = true # version pins are in requirements-dev.lock dev-dependencies = [ "pyright==1.1.399", - "mypy", + "mypy==1.17", "respx", "pytest", "pytest-asyncio", diff --git a/requirements-dev.lock b/requirements-dev.lock index b4fda247..96b88bf2 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -72,7 +72,7 @@ mdurl==0.1.2 multidict==6.4.4 # via aiohttp # via yarl -mypy==1.14.1 +mypy==1.17.0 mypy-extensions==1.0.0 # via mypy nodeenv==1.8.0 @@ -81,6 +81,8 @@ nox==2023.4.22 packaging==23.2 # via nox # via pytest +pathspec==0.12.1 + # via mypy platformdirs==3.11.0 # via virtualenv pluggy==1.5.0 diff --git a/requirements.lock b/requirements.lock index cb9c5ae2..2b76abb6 100644 --- a/requirements.lock +++ b/requirements.lock @@ -55,21 +55,21 @@ multidict==6.4.4 propcache==0.3.1 # via aiohttp # via yarl -pydantic==2.11.9 +pydantic==2.12.5 # via writer-sdk -pydantic-core==2.33.2 +pydantic-core==2.41.5 # via pydantic sniffio==1.3.0 # via anyio # via writer-sdk -typing-extensions==4.12.2 +typing-extensions==4.15.0 # via anyio # via multidict # via pydantic # via pydantic-core # via typing-inspection # via writer-sdk -typing-inspection==0.4.1 +typing-inspection==0.4.2 # via pydantic yarl==1.20.0 # via aiohttp From e59c7b1094e2d8bb7869dc3484bc0a9b2688f642 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 30 Nov 2025 00:58:18 +0000 Subject: [PATCH 336/399] docs(api): updates to API spec --- .stats.yml | 4 +-- src/writerai/resources/files.py | 28 +++++++++++++++++-- src/writerai/resources/vision.py | 12 +++++--- src/writerai/types/file.py | 8 +++++- src/writerai/types/file_upload_params.py | 10 +++++++ src/writerai/types/shared/tool_param.py | 10 +++++-- .../types/shared_params/tool_param.py | 10 +++++-- src/writerai/types/vision_analyze_params.py | 4 +-- tests/api_resources/test_files.py | 20 +++++++++++++ 9 files changed, 89 insertions(+), 17 deletions(-) diff --git a/.stats.yml b/.stats.yml index 9ff96150..33ea12a4 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 33 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-4ec783072dd7f57c6e021a746df7650fb8d7a164d8ec25c7d5cab06c33bc114f.yml -openapi_spec_hash: ceab065d515f3681b0c33137da308968 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-ea6ec4b34f6b7fdecc564f59b2e31482eee05830bf8dc1f389461b158de1548e.yml +openapi_spec_hash: ea89c1faed473908be2740efe6da255f config_hash: 886645f89dc98f04b8931eaf02854e5f diff --git a/src/writerai/resources/files.py b/src/writerai/resources/files.py index cc7c1a78..5a5599c7 100644 --- a/src/writerai/resources/files.py +++ b/src/writerai/resources/files.py @@ -273,6 +273,7 @@ def upload( *, content: FileTypes, content_disposition: str, + graph_id: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -286,6 +287,13 @@ def upload( DOC, DOCX, PPT, PPTX, JPG, PNG, EML, HTML, SRT, CSV, XLS, and XLSX. Args: + graph_id: The unique identifier of the Knowledge Graph to associate the uploaded file + with. + + Note: The response from the upload endpoint does not include the `graphId` + field, but the association will be visible when you retrieve the file using the + file retrieval endpoint. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -299,7 +307,11 @@ def upload( "/v1/files", body=maybe_transform(content, file_upload_params.FileUploadParams), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform({"graph_id": graph_id}, file_upload_params.FileUploadParams), ), cast_to=File, ) @@ -544,6 +556,7 @@ async def upload( *, content: FileTypes, content_disposition: str, + graph_id: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -557,6 +570,13 @@ async def upload( DOC, DOCX, PPT, PPTX, JPG, PNG, EML, HTML, SRT, CSV, XLS, and XLSX. Args: + graph_id: The unique identifier of the Knowledge Graph to associate the uploaded file + with. + + Note: The response from the upload endpoint does not include the `graphId` + field, but the association will be visible when you retrieve the file using the + file retrieval endpoint. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -570,7 +590,11 @@ async def upload( "/v1/files", body=await async_maybe_transform(content, file_upload_params.FileUploadParams), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform({"graph_id": graph_id}, file_upload_params.FileUploadParams), ), cast_to=File, ) diff --git a/src/writerai/resources/vision.py b/src/writerai/resources/vision.py index d90bf3d3..446b31d2 100644 --- a/src/writerai/resources/vision.py +++ b/src/writerai/resources/vision.py @@ -57,8 +57,10 @@ def analyze( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> VisionResponse: - """ - Submit images and a prompt to generate an analysis of the images. + """Submit images and documents with a prompt to generate an analysis. + + Supports JPG, + PNG, PDF, and TXT files up to 7MB each. Args: model: The model to use for image analysis. @@ -125,8 +127,10 @@ async def analyze( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> VisionResponse: - """ - Submit images and a prompt to generate an analysis of the images. + """Submit images and documents with a prompt to generate an analysis. + + Supports JPG, + PNG, PDF, and TXT files up to 7MB each. Args: model: The model to use for image analysis. diff --git a/src/writerai/types/file.py b/src/writerai/types/file.py index f12ba530..8b129976 100644 --- a/src/writerai/types/file.py +++ b/src/writerai/types/file.py @@ -16,7 +16,13 @@ class File(BaseModel): """The timestamp when the file was uploaded.""" graph_ids: List[str] - """A list of Knowledge Graph IDs that the file is associated with.""" + """A list of Knowledge Graph IDs that the file is associated with. + + If you provided a `graphId` during upload, the file is associated with that + Knowledge Graph. However, the `graph_ids` field in the upload response is an + empty list. The association will be visible in the `graph_ids` list when you + retrieve the file using the file retrieval endpoint. + """ name: str """The name of the file.""" diff --git a/src/writerai/types/file_upload_params.py b/src/writerai/types/file_upload_params.py index 760021f9..0487e974 100644 --- a/src/writerai/types/file_upload_params.py +++ b/src/writerai/types/file_upload_params.py @@ -14,3 +14,13 @@ class FileUploadParams(TypedDict, total=False): content: Required[FileTypes] content_disposition: Required[Annotated[str, PropertyInfo(alias="Content-Disposition")]] + + graph_id: Annotated[str, PropertyInfo(alias="graphId")] + """ + The unique identifier of the Knowledge Graph to associate the uploaded file + with. + + Note: The response from the upload endpoint does not include the `graphId` + field, but the association will be visible when you retrieve the file using the + file retrieval endpoint. + """ diff --git a/src/writerai/types/shared/tool_param.py b/src/writerai/types/shared/tool_param.py index c88d8aec..934e6bf4 100644 --- a/src/writerai/types/shared/tool_param.py +++ b/src/writerai/types/shared/tool_param.py @@ -203,10 +203,11 @@ class TranslationTool(BaseModel): class VisionToolFunctionVariable(BaseModel): file_id: str - """The File ID of the image to analyze. + """The File ID of the file to analyze. The file must be uploaded to the Writer platform before you use it with the - Vision tool. The maximum allowed file size is 7MB. + Vision tool. Supported file types: JPG, PNG, PDF, TXT. The maximum allowed file + size is 7MB. """ name: str @@ -228,7 +229,10 @@ class VisionToolFunction(BaseModel): class VisionTool(BaseModel): function: VisionToolFunction - """A tool that uses Palmyra Vision to analyze images.""" + """A tool that uses Palmyra Vision to analyze images and documents. + + Supports JPG, PNG, PDF, and TXT files up to 7MB each. + """ type: Literal["vision"] """The type of tool.""" diff --git a/src/writerai/types/shared_params/tool_param.py b/src/writerai/types/shared_params/tool_param.py index c881bcb5..2ab4c094 100644 --- a/src/writerai/types/shared_params/tool_param.py +++ b/src/writerai/types/shared_params/tool_param.py @@ -204,10 +204,11 @@ class TranslationTool(TypedDict, total=False): class VisionToolFunctionVariable(TypedDict, total=False): file_id: Required[str] - """The File ID of the image to analyze. + """The File ID of the file to analyze. The file must be uploaded to the Writer platform before you use it with the - Vision tool. The maximum allowed file size is 7MB. + Vision tool. Supported file types: JPG, PNG, PDF, TXT. The maximum allowed file + size is 7MB. """ name: Required[str] @@ -229,7 +230,10 @@ class VisionToolFunction(TypedDict, total=False): class VisionTool(TypedDict, total=False): function: Required[VisionToolFunction] - """A tool that uses Palmyra Vision to analyze images.""" + """A tool that uses Palmyra Vision to analyze images and documents. + + Supports JPG, PNG, PDF, and TXT files up to 7MB each. + """ type: Required[Literal["vision"]] """The type of tool.""" diff --git a/src/writerai/types/vision_analyze_params.py b/src/writerai/types/vision_analyze_params.py index 9dac31ac..1bcd12a4 100644 --- a/src/writerai/types/vision_analyze_params.py +++ b/src/writerai/types/vision_analyze_params.py @@ -25,10 +25,10 @@ class VisionAnalyzeParams(TypedDict, total=False): class Variable(TypedDict, total=False): file_id: Required[str] - """The File ID of the image to analyze. + """The File ID of the file to analyze. The file must be uploaded to the Writer platform before it can be used in a - vision request. + vision request. Supported file types: JPG, PNG, PDF, TXT (max 7MB each). """ name: Required[str] diff --git a/tests/api_resources/test_files.py b/tests/api_resources/test_files.py index 142987d1..6f4118b4 100644 --- a/tests/api_resources/test_files.py +++ b/tests/api_resources/test_files.py @@ -238,6 +238,16 @@ def test_method_upload(self, client: Writer) -> None: ) assert_matches_type(File, file, path=["response"]) + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + def test_method_upload_with_all_params(self, client: Writer) -> None: + file = client.files.upload( + content=b"raw file contents", + content_disposition="Content-Disposition", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(File, file, path=["response"]) + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") @parametrize def test_raw_response_upload(self, client: Writer) -> None: @@ -480,6 +490,16 @@ async def test_method_upload(self, async_client: AsyncWriter) -> None: ) assert_matches_type(File, file, path=["response"]) + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + async def test_method_upload_with_all_params(self, async_client: AsyncWriter) -> None: + file = await async_client.files.upload( + content=b"raw file contents", + content_disposition="Content-Disposition", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(File, file, path=["response"]) + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") @parametrize async def test_raw_response_upload(self, async_client: AsyncWriter) -> None: From 5a4dab4f52d81e068b1407f0e0418e5ac2ba54de Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 21:18:24 +0000 Subject: [PATCH 337/399] chore(internal): version bump --- .release-please-manifest.json | 2 +- README.md | 4 ++-- pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index c5e4ca3d..7f554478 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.3.2" + ".": "2.3.3-rc1" } \ No newline at end of file diff --git a/README.md b/README.md index 2619cb28..cc6f0bf3 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ The REST API documentation can be found on [dev.writer.com](https://dev.writer.c ```sh # install from PyPI -pip install writer-sdk +pip install --pre writer-sdk ``` ## Usage @@ -89,7 +89,7 @@ You can enable this by installing `aiohttp`: ```sh # install from PyPI -pip install writer-sdk[aiohttp] +pip install --pre writer-sdk[aiohttp] ``` Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: diff --git a/pyproject.toml b/pyproject.toml index 093bd415..e34d25c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "2.3.2" +version = "2.3.3-rc1" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index 062565c2..6ff9a512 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "2.3.2" # x-release-please-version +__version__ = "2.3.3-rc1" # x-release-please-version From 6d6b6cf0b68ec55983c21dfa522ae3c49781d0bd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 11:07:38 +0000 Subject: [PATCH 338/399] chore: update lockfile --- pyproject.toml | 14 +++--- requirements-dev.lock | 108 +++++++++++++++++++++++------------------- requirements.lock | 31 ++++++------ 3 files changed, 83 insertions(+), 70 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e34d25c2..92e69a44 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,14 +7,16 @@ license = "Apache-2.0" authors = [ { name = "Writer", email = "dev-feedback@writer.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.9" classifiers = [ "Typing :: Typed", diff --git a/requirements-dev.lock b/requirements-dev.lock index 96b88bf2..f2bde9e2 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -12,40 +12,45 @@ -e file:. aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.12.8 +aiohttp==3.13.2 # via httpx-aiohttp # via writer-sdk -aiosignal==1.3.2 +aiosignal==1.4.0 # via aiohttp -annotated-types==0.6.0 +annotated-types==0.7.0 # via pydantic -anyio==4.4.0 +anyio==4.12.0 # via httpx # via writer-sdk -argcomplete==3.1.2 +argcomplete==3.6.3 # via nox async-timeout==5.0.1 # via aiohttp -attrs==25.3.0 +attrs==25.4.0 # via aiohttp -certifi==2023.7.22 + # via nox +backports-asyncio-runner==1.2.0 + # via pytest-asyncio +certifi==2025.11.12 # via httpcore # via httpx -colorlog==6.7.0 +colorlog==6.10.1 + # via nox +dependency-groups==1.3.1 # via nox -dirty-equals==0.6.0 -distlib==0.3.7 +dirty-equals==0.11 +distlib==0.4.0 # via virtualenv -distro==1.8.0 +distro==1.9.0 # via writer-sdk -exceptiongroup==1.2.2 +exceptiongroup==1.3.1 # via anyio # via pytest -execnet==2.1.1 +execnet==2.1.2 # via pytest-xdist -filelock==3.12.4 +filelock==3.19.1 # via virtualenv -frozenlist==1.6.2 +frozenlist==1.8.0 # via aiohttp # via aiosignal h11==0.16.0 @@ -58,82 +63,87 @@ httpx==0.28.1 # via writer-sdk httpx-aiohttp==0.1.9 # via writer-sdk -idna==3.4 +humanize==4.13.0 + # via nox +idna==3.11 # via anyio # via httpx # via yarl -importlib-metadata==7.0.0 -iniconfig==2.0.0 +importlib-metadata==8.7.0 +iniconfig==2.1.0 # via pytest markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -multidict==6.4.4 +multidict==6.7.0 # via aiohttp # via yarl mypy==1.17.0 -mypy-extensions==1.0.0 +mypy-extensions==1.1.0 # via mypy -nodeenv==1.8.0 +nodeenv==1.9.1 # via pyright -nox==2023.4.22 -packaging==23.2 +nox==2025.11.12 +packaging==25.0 + # via dependency-groups # via nox # via pytest pathspec==0.12.1 # via mypy -platformdirs==3.11.0 +platformdirs==4.4.0 # via virtualenv -pluggy==1.5.0 +pluggy==1.6.0 # via pytest -propcache==0.3.1 +propcache==0.4.1 # via aiohttp # via yarl -pydantic==2.11.9 +pydantic==2.12.5 # via writer-sdk -pydantic-core==2.33.2 +pydantic-core==2.41.5 # via pydantic -pygments==2.18.0 +pygments==2.19.2 + # via pytest # via rich pyright==1.1.399 -pytest==8.3.3 +pytest==8.4.2 # via pytest-asyncio # via pytest-xdist -pytest-asyncio==0.24.0 -pytest-xdist==3.7.0 -python-dateutil==2.8.2 +pytest-asyncio==1.2.0 +pytest-xdist==3.8.0 +python-dateutil==2.9.0.post0 # via time-machine -pytz==2023.3.post1 - # via dirty-equals respx==0.22.0 -rich==13.7.1 -ruff==0.9.4 -setuptools==68.2.2 - # via nodeenv -six==1.16.0 +rich==14.2.0 +ruff==0.14.7 +six==1.17.0 # via python-dateutil -sniffio==1.3.0 - # via anyio +sniffio==1.3.1 # via writer-sdk -time-machine==2.9.0 -tomli==2.0.2 +time-machine==2.19.0 +tomli==2.3.0 + # via dependency-groups # via mypy + # via nox # via pytest -typing-extensions==4.12.2 +typing-extensions==4.15.0 + # via aiosignal # via anyio + # via exceptiongroup # via multidict # via mypy # via pydantic # via pydantic-core # via pyright + # via pytest-asyncio # via typing-inspection + # via virtualenv # via writer-sdk -typing-inspection==0.4.1 +typing-inspection==0.4.2 # via pydantic -virtualenv==20.24.5 +virtualenv==20.35.4 # via nox -yarl==1.20.0 +yarl==1.22.0 # via aiohttp -zipp==3.17.0 +zipp==3.23.0 # via importlib-metadata diff --git a/requirements.lock b/requirements.lock index 2b76abb6..90343d3b 100644 --- a/requirements.lock +++ b/requirements.lock @@ -12,28 +12,28 @@ -e file:. aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.12.8 +aiohttp==3.13.2 # via httpx-aiohttp # via writer-sdk -aiosignal==1.3.2 +aiosignal==1.4.0 # via aiohttp -annotated-types==0.6.0 +annotated-types==0.7.0 # via pydantic -anyio==4.4.0 +anyio==4.12.0 # via httpx # via writer-sdk async-timeout==5.0.1 # via aiohttp -attrs==25.3.0 +attrs==25.4.0 # via aiohttp -certifi==2023.7.22 +certifi==2025.11.12 # via httpcore # via httpx -distro==1.8.0 +distro==1.9.0 # via writer-sdk -exceptiongroup==1.2.2 +exceptiongroup==1.3.1 # via anyio -frozenlist==1.6.2 +frozenlist==1.8.0 # via aiohttp # via aiosignal h11==0.16.0 @@ -45,25 +45,26 @@ httpx==0.28.1 # via writer-sdk httpx-aiohttp==0.1.9 # via writer-sdk -idna==3.4 +idna==3.11 # via anyio # via httpx # via yarl -multidict==6.4.4 +multidict==6.7.0 # via aiohttp # via yarl -propcache==0.3.1 +propcache==0.4.1 # via aiohttp # via yarl pydantic==2.12.5 # via writer-sdk pydantic-core==2.41.5 # via pydantic -sniffio==1.3.0 - # via anyio +sniffio==1.3.1 # via writer-sdk typing-extensions==4.15.0 + # via aiosignal # via anyio + # via exceptiongroup # via multidict # via pydantic # via pydantic-core @@ -71,5 +72,5 @@ typing-extensions==4.15.0 # via writer-sdk typing-inspection==0.4.2 # via pydantic -yarl==1.20.0 +yarl==1.22.0 # via aiohttp From 98c3e694d4a0e2ff3982ed13f3b93730ce23c794 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 21:24:49 +0000 Subject: [PATCH 339/399] chore(docs): use environment variables for authentication in code snippets --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cc6f0bf3..15adecc5 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,7 @@ pip install --pre writer-sdk[aiohttp] Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: ```python +import os import asyncio from writerai import DefaultAioHttpClient from writerai import AsyncWriter @@ -102,7 +103,7 @@ from writerai import AsyncWriter async def main() -> None: async with AsyncWriter( - api_key="My API Key", + api_key=os.environ.get("WRITER_API_KEY"), # This is the default and can be omitted http_client=DefaultAioHttpClient(), ) as client: chat_completion = await client.chat.chat( From 7fe636f970a31a975822d6121450e61e45084fc8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 18:38:48 +0000 Subject: [PATCH 340/399] fix(types): allow pyright to infer TypedDict types within SequenceNotStr --- src/writerai/_types.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/writerai/_types.py b/src/writerai/_types.py index 146fcfb6..ed3a7f53 100644 --- a/src/writerai/_types.py +++ b/src/writerai/_types.py @@ -243,6 +243,9 @@ class HttpxSendArgs(TypedDict, total=False): if TYPE_CHECKING: # This works because str.__contains__ does not accept object (either in typeshed or at runtime) # https://github.com/hauntsaninja/useful_types/blob/5e9710f3875107d068e7679fd7fec9cfab0eff3b/useful_types/__init__.py#L285 + # + # Note: index() and count() methods are intentionally omitted to allow pyright to properly + # infer TypedDict types when dict literals are used in lists assigned to SequenceNotStr. class SequenceNotStr(Protocol[_T_co]): @overload def __getitem__(self, index: SupportsIndex, /) -> _T_co: ... @@ -251,8 +254,6 @@ def __getitem__(self, index: slice, /) -> Sequence[_T_co]: ... def __contains__(self, value: object, /) -> bool: ... def __len__(self) -> int: ... def __iter__(self) -> Iterator[_T_co]: ... - def index(self, value: Any, start: int = 0, stop: int = ..., /) -> int: ... - def count(self, value: Any, /) -> int: ... def __reversed__(self) -> Iterator[_T_co]: ... else: # just point this to a normal `Sequence` at runtime to avoid having to special case From 51204f29dda1f978d423e1dd9341d9ad43c18946 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 19:29:02 +0000 Subject: [PATCH 341/399] chore: add missing docstrings --- .../types/application_list_response.py | 12 ++++++++++ .../types/application_retrieve_response.py | 12 ++++++++++ src/writerai/types/chat_chat_params.py | 17 ++++++++++++++ src/writerai/types/chat_completion_chunk.py | 2 ++ src/writerai/types/chat_completion_message.py | 5 +++++ src/writerai/types/chat_completion_usage.py | 5 +++++ src/writerai/types/graph.py | 4 ++++ src/writerai/types/graph_create_response.py | 2 ++ src/writerai/types/graph_question_params.py | 4 ++++ src/writerai/types/graph_update_response.py | 2 ++ src/writerai/types/question.py | 16 ++++++++++++++ .../types/shared/function_definition.py | 2 ++ src/writerai/types/shared/graph_data.py | 16 ++++++++++++++ src/writerai/types/shared/logprobs_token.py | 4 ++++ src/writerai/types/shared/source.py | 2 ++ src/writerai/types/shared/tool_param.py | 22 +++++++++++++++++++ .../shared_params/function_definition.py | 2 ++ .../types/shared_params/graph_data.py | 16 ++++++++++++++ src/writerai/types/shared_params/source.py | 2 ++ .../types/shared_params/tool_param.py | 22 +++++++++++++++++++ src/writerai/types/vision_analyze_params.py | 7 ++++++ 21 files changed, 176 insertions(+) diff --git a/src/writerai/types/application_list_response.py b/src/writerai/types/application_list_response.py index e80b6e90..d6eb0734 100644 --- a/src/writerai/types/application_list_response.py +++ b/src/writerai/types/application_list_response.py @@ -18,11 +18,15 @@ class InputOptionsApplicationInputDropdownOptions(BaseModel): + """Configuration options specific to dropdown-type input fields.""" + list: List[str] """List of available options in the dropdown menu.""" class InputOptionsApplicationInputFileOptions(BaseModel): + """Configuration options specific to file upload input fields.""" + file_types: List[str] """List of allowed file extensions.""" @@ -40,6 +44,8 @@ class InputOptionsApplicationInputFileOptions(BaseModel): class InputOptionsApplicationInputMediaOptions(BaseModel): + """Configuration options specific to media upload input fields.""" + file_types: List[str] """List of allowed media file types.""" @@ -48,6 +54,8 @@ class InputOptionsApplicationInputMediaOptions(BaseModel): class InputOptionsApplicationInputTextOptions(BaseModel): + """Configuration options specific to text input fields.""" + max_fields: int """Maximum number of text fields allowed.""" @@ -64,6 +72,8 @@ class InputOptionsApplicationInputTextOptions(BaseModel): class Input(BaseModel): + """Configuration for an individual input field in the application.""" + input_type: Literal["text", "dropdown", "file", "media"] """Type of input field determining its behavior and validation rules.""" @@ -81,6 +91,8 @@ class Input(BaseModel): class ApplicationListResponse(BaseModel): + """Detailed application object including its input configuration.""" + id: str """Unique identifier for the application.""" diff --git a/src/writerai/types/application_retrieve_response.py b/src/writerai/types/application_retrieve_response.py index 00827b7a..6fec026a 100644 --- a/src/writerai/types/application_retrieve_response.py +++ b/src/writerai/types/application_retrieve_response.py @@ -18,11 +18,15 @@ class InputOptionsApplicationInputDropdownOptions(BaseModel): + """Configuration options specific to dropdown-type input fields.""" + list: List[str] """List of available options in the dropdown menu.""" class InputOptionsApplicationInputFileOptions(BaseModel): + """Configuration options specific to file upload input fields.""" + file_types: List[str] """List of allowed file extensions.""" @@ -40,6 +44,8 @@ class InputOptionsApplicationInputFileOptions(BaseModel): class InputOptionsApplicationInputMediaOptions(BaseModel): + """Configuration options specific to media upload input fields.""" + file_types: List[str] """List of allowed media file types.""" @@ -48,6 +54,8 @@ class InputOptionsApplicationInputMediaOptions(BaseModel): class InputOptionsApplicationInputTextOptions(BaseModel): + """Configuration options specific to text input fields.""" + max_fields: int """Maximum number of text fields allowed.""" @@ -64,6 +72,8 @@ class InputOptionsApplicationInputTextOptions(BaseModel): class Input(BaseModel): + """Configuration for an individual input field in the application.""" + input_type: Literal["text", "dropdown", "file", "media"] """Type of input field determining its behavior and validation rules.""" @@ -81,6 +91,8 @@ class Input(BaseModel): class ApplicationRetrieveResponse(BaseModel): + """Detailed application object including its input configuration.""" + id: str """Unique identifier for the application.""" diff --git a/src/writerai/types/chat_chat_params.py b/src/writerai/types/chat_chat_params.py index 522248af..b7b3d830 100644 --- a/src/writerai/types/chat_chat_params.py +++ b/src/writerai/types/chat_chat_params.py @@ -122,6 +122,8 @@ class ChatChatParamsBase(TypedDict, total=False): class MessageContentMixedContentTextFragment(TypedDict, total=False): + """Represents a text content fragment within a chat message.""" + text: Required[str] """The actual text content of the message fragment.""" @@ -130,6 +132,8 @@ class MessageContentMixedContentTextFragment(TypedDict, total=False): class MessageContentMixedContentImageFragmentImageURL(TypedDict, total=False): + """The image URL object containing the location of the image.""" + url: Required[str] """The URL pointing to the image file. @@ -138,6 +142,11 @@ class MessageContentMixedContentImageFragmentImageURL(TypedDict, total=False): class MessageContentMixedContentImageFragment(TypedDict, total=False): + """Represents an image content fragment within a chat message. + + Note: This content type is only supported with the Palmyra X5 model. + """ + image_url: Required[MessageContentMixedContentImageFragmentImageURL] """The image URL object containing the location of the image.""" @@ -184,6 +193,12 @@ class Message(TypedDict, total=False): class ResponseFormat(TypedDict, total=False): + """ + The response format to use for the chat completion, available with `palmyra-x4` and `palmyra-x5`. + + `text` is the default response format. [JSON Schema](https://json-schema.org/) is supported for structured responses. If you specify `json_schema`, you must also provide a `json_schema` object. + """ + type: Required[Literal["text", "json_schema"]] """The type of response format to use.""" @@ -192,6 +207,8 @@ class ResponseFormat(TypedDict, total=False): class StreamOptions(TypedDict, total=False): + """Additional options for streaming.""" + include_usage: Required[bool] """Indicate whether to include usage information.""" diff --git a/src/writerai/types/chat_completion_chunk.py b/src/writerai/types/chat_completion_chunk.py index 6d873355..bc3bf85b 100644 --- a/src/writerai/types/chat_completion_chunk.py +++ b/src/writerai/types/chat_completion_chunk.py @@ -33,6 +33,8 @@ class ChoiceDeltaTranslationData(BaseModel): class ChoiceDelta(BaseModel): + """A chat completion delta generated by streamed model responses.""" + content: Optional[str] = None """The text content produced by the model. diff --git a/src/writerai/types/chat_completion_message.py b/src/writerai/types/chat_completion_message.py index 5b4cb30b..97045c61 100644 --- a/src/writerai/types/chat_completion_message.py +++ b/src/writerai/types/chat_completion_message.py @@ -40,6 +40,11 @@ class WebSearchData(BaseModel): class ChatCompletionMessage(BaseModel): + """The chat completion message from the model. + + Note: this field is deprecated for streaming. Use `delta` instead. + """ + content: str """The text content produced by the model. diff --git a/src/writerai/types/chat_completion_usage.py b/src/writerai/types/chat_completion_usage.py index 6fc89018..ec7b7991 100644 --- a/src/writerai/types/chat_completion_usage.py +++ b/src/writerai/types/chat_completion_usage.py @@ -16,6 +16,11 @@ class PromptTokenDetails(BaseModel): class ChatCompletionUsage(BaseModel): + """Usage information for the chat completion response. + + Please note that at this time Knowledge Graph tool usage is not included in this object. + """ + completion_tokens: int prompt_tokens: int diff --git a/src/writerai/types/graph.py b/src/writerai/types/graph.py index 469c0da6..7721c972 100644 --- a/src/writerai/types/graph.py +++ b/src/writerai/types/graph.py @@ -10,6 +10,8 @@ class FileStatus(BaseModel): + """The processing status of files in the Knowledge Graph.""" + completed: int """The number of files that have been successfully processed.""" @@ -24,6 +26,8 @@ class FileStatus(BaseModel): class URLStatus(BaseModel): + """The current status of the URL processing.""" + status: Literal["validating", "success", "error"] """The current status of the URL processing.""" diff --git a/src/writerai/types/graph_create_response.py b/src/writerai/types/graph_create_response.py index 11dcb958..87b471c0 100644 --- a/src/writerai/types/graph_create_response.py +++ b/src/writerai/types/graph_create_response.py @@ -10,6 +10,8 @@ class URLStatus(BaseModel): + """The current status of the URL processing.""" + status: Literal["validating", "success", "error"] """The current status of the URL processing.""" diff --git a/src/writerai/types/graph_question_params.py b/src/writerai/types/graph_question_params.py index a49563c7..dc61ae1d 100644 --- a/src/writerai/types/graph_question_params.py +++ b/src/writerai/types/graph_question_params.py @@ -28,6 +28,10 @@ class GraphQuestionParamsBase(TypedDict, total=False): class QueryConfig(TypedDict, total=False): + """ + Configuration options for Knowledge Graph queries, including search parameters and citation settings. + """ + grounding_level: float """ Level of grounding required for responses, controlling how closely answers must diff --git a/src/writerai/types/graph_update_response.py b/src/writerai/types/graph_update_response.py index bc17ebf3..6910f9d4 100644 --- a/src/writerai/types/graph_update_response.py +++ b/src/writerai/types/graph_update_response.py @@ -10,6 +10,8 @@ class URLStatus(BaseModel): + """The current status of the URL processing.""" + status: Literal["validating", "success", "error"] """The current status of the URL processing.""" diff --git a/src/writerai/types/question.py b/src/writerai/types/question.py index 9f3095bb..58a656cb 100644 --- a/src/writerai/types/question.py +++ b/src/writerai/types/question.py @@ -11,6 +11,10 @@ class ReferencesFile(BaseModel): + """ + A file-based reference containing text snippets from uploaded documents in the Knowledge Graph. + """ + file_id: str = FieldInfo(alias="fileId") """The unique identifier of the file in your Writer account.""" @@ -37,6 +41,10 @@ class ReferencesFile(BaseModel): class ReferencesWeb(BaseModel): + """ + A web-based reference containing text snippets from online sources accessed during the query. + """ + score: float """ Internal score used during the retrieval process for ranking and selecting @@ -57,6 +65,10 @@ class ReferencesWeb(BaseModel): class References(BaseModel): + """ + Detailed source information organized by reference type, providing comprehensive metadata about the sources used to generate the response. + """ + files: Optional[List[ReferencesFile]] = None """Array of file-based references from uploaded documents in the Knowledge Graph.""" @@ -65,6 +77,10 @@ class References(BaseModel): class Subquery(BaseModel): + """ + A sub-question generated to break down complex queries into more manageable parts, along with its answer and supporting sources. + """ + answer: str """The answer to the subquery based on Knowledge Graph content.""" diff --git a/src/writerai/types/shared/function_definition.py b/src/writerai/types/shared/function_definition.py index 357434f5..e71dd5fc 100644 --- a/src/writerai/types/shared/function_definition.py +++ b/src/writerai/types/shared/function_definition.py @@ -9,6 +9,8 @@ class FunctionDefinition(BaseModel): + """A tool that uses a custom function.""" + name: str """Name of the function.""" diff --git a/src/writerai/types/shared/graph_data.py b/src/writerai/types/shared/graph_data.py index 6d8eb341..bac0293d 100644 --- a/src/writerai/types/shared/graph_data.py +++ b/src/writerai/types/shared/graph_data.py @@ -12,6 +12,10 @@ class ReferencesFile(BaseModel): + """ + A file-based reference containing text snippets from uploaded documents in the Knowledge Graph. + """ + file_id: str = FieldInfo(alias="fileId") """The unique identifier of the file in your Writer account.""" @@ -38,6 +42,10 @@ class ReferencesFile(BaseModel): class ReferencesWeb(BaseModel): + """ + A web-based reference containing text snippets from online sources accessed during the query. + """ + score: float """ Internal score used during the retrieval process for ranking and selecting @@ -58,6 +66,10 @@ class ReferencesWeb(BaseModel): class References(BaseModel): + """ + Detailed source information organized by reference type, providing comprehensive metadata about the sources used to generate the response. + """ + files: Optional[List[ReferencesFile]] = None """Array of file-based references from uploaded documents in the Knowledge Graph.""" @@ -66,6 +78,10 @@ class References(BaseModel): class Subquery(BaseModel): + """ + A sub-question generated to break down complex queries into more manageable parts, along with its answer and supporting sources. + """ + answer: str """The answer to the subquery based on Knowledge Graph content.""" diff --git a/src/writerai/types/shared/logprobs_token.py b/src/writerai/types/shared/logprobs_token.py index 40c58d67..cf122336 100644 --- a/src/writerai/types/shared/logprobs_token.py +++ b/src/writerai/types/shared/logprobs_token.py @@ -8,6 +8,10 @@ class TopLogprob(BaseModel): + """ + An array of mappings for each token to its top log probabilities, showing detailed prediction probabilities. + """ + token: str logprob: float diff --git a/src/writerai/types/shared/source.py b/src/writerai/types/shared/source.py index 326d01ad..65debe9c 100644 --- a/src/writerai/types/shared/source.py +++ b/src/writerai/types/shared/source.py @@ -6,6 +6,8 @@ class Source(BaseModel): + """A source snippet containing text and fileId from Knowledge Graph content.""" + file_id: str """The unique identifier of the file in your Writer account.""" diff --git a/src/writerai/types/shared/tool_param.py b/src/writerai/types/shared/tool_param.py index 934e6bf4..1391010a 100644 --- a/src/writerai/types/shared/tool_param.py +++ b/src/writerai/types/shared/tool_param.py @@ -34,6 +34,10 @@ class FunctionTool(BaseModel): class GraphToolFunctionQueryConfig(BaseModel): + """ + Configuration options for Knowledge Graph queries, including search parameters and citation settings. + """ + grounding_level: Optional[float] = None """ Level of grounding required for responses, controlling how closely answers must @@ -101,6 +105,8 @@ class GraphToolFunctionQueryConfig(BaseModel): class GraphToolFunction(BaseModel): + """A tool that uses Knowledge Graphs as context for responses.""" + graph_ids: List[str] """An array of graph IDs to use in the tool.""" @@ -126,6 +132,8 @@ class GraphTool(BaseModel): class LlmToolFunction(BaseModel): + """A tool that uses another Writer model to generate a response.""" + description: str """A description of the model to use.""" @@ -142,6 +150,8 @@ class LlmTool(BaseModel): class TranslationToolFunction(BaseModel): + """A tool that uses Palmyra Translate to translate text.""" + formality: bool """Whether to use formal or informal language in the translation. @@ -194,6 +204,11 @@ class TranslationToolFunction(BaseModel): class TranslationTool(BaseModel): + """A tool that uses Palmyra Translate to translate text. + + Note that this tool does not stream results. The response is returned after the translation is complete. + """ + function: TranslationToolFunction """A tool that uses Palmyra Translate to translate text.""" @@ -221,6 +236,11 @@ class VisionToolFunctionVariable(BaseModel): class VisionToolFunction(BaseModel): + """A tool that uses Palmyra Vision to analyze images and documents. + + Supports JPG, PNG, PDF, and TXT files up to 7MB each. + """ + model: Literal["palmyra-vision"] """The model to use for image analysis.""" @@ -239,6 +259,8 @@ class VisionTool(BaseModel): class WebSearchToolFunction(BaseModel): + """A tool that uses web search to find information.""" + exclude_domains: List[str] """An array of domains to exclude from the search results.""" diff --git a/src/writerai/types/shared_params/function_definition.py b/src/writerai/types/shared_params/function_definition.py index 53606cb6..44dcd0c7 100644 --- a/src/writerai/types/shared_params/function_definition.py +++ b/src/writerai/types/shared_params/function_definition.py @@ -10,6 +10,8 @@ class FunctionDefinition(TypedDict, total=False): + """A tool that uses a custom function.""" + name: Required[str] """Name of the function.""" diff --git a/src/writerai/types/shared_params/graph_data.py b/src/writerai/types/shared_params/graph_data.py index 6e162c1d..70f0caad 100644 --- a/src/writerai/types/shared_params/graph_data.py +++ b/src/writerai/types/shared_params/graph_data.py @@ -12,6 +12,10 @@ class ReferencesFile(TypedDict, total=False): + """ + A file-based reference containing text snippets from uploaded documents in the Knowledge Graph. + """ + file_id: Required[Annotated[str, PropertyInfo(alias="fileId")]] """The unique identifier of the file in your Writer account.""" @@ -38,6 +42,10 @@ class ReferencesFile(TypedDict, total=False): class ReferencesWeb(TypedDict, total=False): + """ + A web-based reference containing text snippets from online sources accessed during the query. + """ + score: Required[float] """ Internal score used during the retrieval process for ranking and selecting @@ -58,6 +66,10 @@ class ReferencesWeb(TypedDict, total=False): class References(TypedDict, total=False): + """ + Detailed source information organized by reference type, providing comprehensive metadata about the sources used to generate the response. + """ + files: Iterable[ReferencesFile] """Array of file-based references from uploaded documents in the Knowledge Graph.""" @@ -66,6 +78,10 @@ class References(TypedDict, total=False): class Subquery(TypedDict, total=False): + """ + A sub-question generated to break down complex queries into more manageable parts, along with its answer and supporting sources. + """ + answer: Required[str] """The answer to the subquery based on Knowledge Graph content.""" diff --git a/src/writerai/types/shared_params/source.py b/src/writerai/types/shared_params/source.py index dc6726ba..a1397fb5 100644 --- a/src/writerai/types/shared_params/source.py +++ b/src/writerai/types/shared_params/source.py @@ -8,6 +8,8 @@ class Source(TypedDict, total=False): + """A source snippet containing text and fileId from Knowledge Graph content.""" + file_id: Required[str] """The unique identifier of the file in your Writer account.""" diff --git a/src/writerai/types/shared_params/tool_param.py b/src/writerai/types/shared_params/tool_param.py index 2ab4c094..1a9c4dd7 100644 --- a/src/writerai/types/shared_params/tool_param.py +++ b/src/writerai/types/shared_params/tool_param.py @@ -35,6 +35,10 @@ class FunctionTool(TypedDict, total=False): class GraphToolFunctionQueryConfig(TypedDict, total=False): + """ + Configuration options for Knowledge Graph queries, including search parameters and citation settings. + """ + grounding_level: float """ Level of grounding required for responses, controlling how closely answers must @@ -102,6 +106,8 @@ class GraphToolFunctionQueryConfig(TypedDict, total=False): class GraphToolFunction(TypedDict, total=False): + """A tool that uses Knowledge Graphs as context for responses.""" + graph_ids: Required[SequenceNotStr[str]] """An array of graph IDs to use in the tool.""" @@ -127,6 +133,8 @@ class GraphTool(TypedDict, total=False): class LlmToolFunction(TypedDict, total=False): + """A tool that uses another Writer model to generate a response.""" + description: Required[str] """A description of the model to use.""" @@ -143,6 +151,8 @@ class LlmTool(TypedDict, total=False): class TranslationToolFunction(TypedDict, total=False): + """A tool that uses Palmyra Translate to translate text.""" + formality: Required[bool] """Whether to use formal or informal language in the translation. @@ -195,6 +205,11 @@ class TranslationToolFunction(TypedDict, total=False): class TranslationTool(TypedDict, total=False): + """A tool that uses Palmyra Translate to translate text. + + Note that this tool does not stream results. The response is returned after the translation is complete. + """ + function: Required[TranslationToolFunction] """A tool that uses Palmyra Translate to translate text.""" @@ -222,6 +237,11 @@ class VisionToolFunctionVariable(TypedDict, total=False): class VisionToolFunction(TypedDict, total=False): + """A tool that uses Palmyra Vision to analyze images and documents. + + Supports JPG, PNG, PDF, and TXT files up to 7MB each. + """ + model: Required[Literal["palmyra-vision"]] """The model to use for image analysis.""" @@ -240,6 +260,8 @@ class VisionTool(TypedDict, total=False): class WebSearchToolFunction(TypedDict, total=False): + """A tool that uses web search to find information.""" + exclude_domains: Required[SequenceNotStr[str]] """An array of domains to exclude from the search results.""" diff --git a/src/writerai/types/vision_analyze_params.py b/src/writerai/types/vision_analyze_params.py index 1bcd12a4..2e0adf2d 100644 --- a/src/writerai/types/vision_analyze_params.py +++ b/src/writerai/types/vision_analyze_params.py @@ -24,6 +24,13 @@ class VisionAnalyzeParams(TypedDict, total=False): class Variable(TypedDict, total=False): + """An array of file variables required for the analysis. + + The files must be uploaded to the Writer platform before they can be used in a vision request. Learn how to upload files using the [Files API](https://dev.writer.com/api-reference/file-api/upload-files). + + Supported file types: JPG, PNG, PDF, TXT. The maximum allowed file size for each file is 7MB. + """ + file_id: Required[str] """The File ID of the file to analyze. From 9fa8537517346fa847777899cbf9a7023ce2cd02 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 19:21:34 +0000 Subject: [PATCH 342/399] chore(internal): add missing files argument to base client --- src/writerai/_base_client.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index 1bee460f..a0b81924 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -1247,9 +1247,12 @@ def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + opts = FinalRequestOptions.construct( + method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + ) return self.request(cast_to, opts) def put( @@ -1767,9 +1770,12 @@ async def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + opts = FinalRequestOptions.construct( + method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + ) return await self.request(cast_to, opts) async def put( From 29b2a51fbe3cebfff030f6a9071d57b2447bc71e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 17:23:00 +0000 Subject: [PATCH 343/399] chore: speedup initial import --- src/writerai/_client.py | 450 ++++++++++++++++++++++++++++++++-------- 1 file changed, 364 insertions(+), 86 deletions(-) diff --git a/src/writerai/_client.py b/src/writerai/_client.py index a6f38d8f..c4d23f42 100644 --- a/src/writerai/_client.py +++ b/src/writerai/_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 @@ -20,8 +20,8 @@ not_given, ) from ._utils import is_given, get_async_library +from ._compat import cached_property from ._version import __version__ -from .resources import chat, files, graphs, models, vision, completions, translation from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import WriterError, APIStatusError from ._base_client import ( @@ -29,25 +29,23 @@ SyncAPIClient, AsyncAPIClient, ) -from .resources.tools import tools -from .resources.applications import applications + +if TYPE_CHECKING: + from .resources import chat, files, tools, graphs, models, vision, completions, translation, applications + from .resources.chat import ChatResource, AsyncChatResource + from .resources.files import FilesResource, AsyncFilesResource + from .resources.graphs import GraphsResource, AsyncGraphsResource + from .resources.models import ModelsResource, AsyncModelsResource + from .resources.vision import VisionResource, AsyncVisionResource + from .resources.completions import CompletionsResource, AsyncCompletionsResource + from .resources.tools.tools import ToolsResource, AsyncToolsResource + from .resources.translation import TranslationResource, AsyncTranslationResource + from .resources.applications.applications import ApplicationsResource, AsyncApplicationsResource __all__ = ["Timeout", "Transport", "ProxiesTypes", "RequestOptions", "Writer", "AsyncWriter", "Client", "AsyncClient"] class Writer(SyncAPIClient): - applications: applications.ApplicationsResource - chat: chat.ChatResource - completions: completions.CompletionsResource - models: models.ModelsResource - graphs: graphs.GraphsResource - files: files.FilesResource - tools: tools.ToolsResource - translation: translation.TranslationResource - vision: vision.VisionResource - with_raw_response: WriterWithRawResponse - with_streaming_response: WriterWithStreamedResponse - # client options api_key: str @@ -104,17 +102,67 @@ def __init__( self._default_stream_cls = Stream - self.applications = applications.ApplicationsResource(self) - self.chat = chat.ChatResource(self) - self.completions = completions.CompletionsResource(self) - self.models = models.ModelsResource(self) - self.graphs = graphs.GraphsResource(self) - self.files = files.FilesResource(self) - self.tools = tools.ToolsResource(self) - self.translation = translation.TranslationResource(self) - self.vision = vision.VisionResource(self) - self.with_raw_response = WriterWithRawResponse(self) - self.with_streaming_response = WriterWithStreamedResponse(self) + @cached_property + def applications(self) -> ApplicationsResource: + from .resources.applications import ApplicationsResource + + return ApplicationsResource(self) + + @cached_property + def chat(self) -> ChatResource: + from .resources.chat import ChatResource + + return ChatResource(self) + + @cached_property + def completions(self) -> CompletionsResource: + from .resources.completions import CompletionsResource + + return CompletionsResource(self) + + @cached_property + def models(self) -> ModelsResource: + from .resources.models import ModelsResource + + return ModelsResource(self) + + @cached_property + def graphs(self) -> GraphsResource: + from .resources.graphs import GraphsResource + + return GraphsResource(self) + + @cached_property + def files(self) -> FilesResource: + from .resources.files import FilesResource + + return FilesResource(self) + + @cached_property + def tools(self) -> ToolsResource: + from .resources.tools import ToolsResource + + return ToolsResource(self) + + @cached_property + def translation(self) -> TranslationResource: + from .resources.translation import TranslationResource + + return TranslationResource(self) + + @cached_property + def vision(self) -> VisionResource: + from .resources.vision import VisionResource + + return VisionResource(self) + + @cached_property + def with_raw_response(self) -> WriterWithRawResponse: + return WriterWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> WriterWithStreamedResponse: + return WriterWithStreamedResponse(self) @property @override @@ -222,18 +270,6 @@ def _make_status_error( class AsyncWriter(AsyncAPIClient): - applications: applications.AsyncApplicationsResource - chat: chat.AsyncChatResource - completions: completions.AsyncCompletionsResource - models: models.AsyncModelsResource - graphs: graphs.AsyncGraphsResource - files: files.AsyncFilesResource - tools: tools.AsyncToolsResource - translation: translation.AsyncTranslationResource - vision: vision.AsyncVisionResource - with_raw_response: AsyncWriterWithRawResponse - with_streaming_response: AsyncWriterWithStreamedResponse - # client options api_key: str @@ -290,17 +326,67 @@ def __init__( self._default_stream_cls = AsyncStream - self.applications = applications.AsyncApplicationsResource(self) - self.chat = chat.AsyncChatResource(self) - self.completions = completions.AsyncCompletionsResource(self) - self.models = models.AsyncModelsResource(self) - self.graphs = graphs.AsyncGraphsResource(self) - self.files = files.AsyncFilesResource(self) - self.tools = tools.AsyncToolsResource(self) - self.translation = translation.AsyncTranslationResource(self) - self.vision = vision.AsyncVisionResource(self) - self.with_raw_response = AsyncWriterWithRawResponse(self) - self.with_streaming_response = AsyncWriterWithStreamedResponse(self) + @cached_property + def applications(self) -> AsyncApplicationsResource: + from .resources.applications import AsyncApplicationsResource + + return AsyncApplicationsResource(self) + + @cached_property + def chat(self) -> AsyncChatResource: + from .resources.chat import AsyncChatResource + + return AsyncChatResource(self) + + @cached_property + def completions(self) -> AsyncCompletionsResource: + from .resources.completions import AsyncCompletionsResource + + return AsyncCompletionsResource(self) + + @cached_property + def models(self) -> AsyncModelsResource: + from .resources.models import AsyncModelsResource + + return AsyncModelsResource(self) + + @cached_property + def graphs(self) -> AsyncGraphsResource: + from .resources.graphs import AsyncGraphsResource + + return AsyncGraphsResource(self) + + @cached_property + def files(self) -> AsyncFilesResource: + from .resources.files import AsyncFilesResource + + return AsyncFilesResource(self) + + @cached_property + def tools(self) -> AsyncToolsResource: + from .resources.tools import AsyncToolsResource + + return AsyncToolsResource(self) + + @cached_property + def translation(self) -> AsyncTranslationResource: + from .resources.translation import AsyncTranslationResource + + return AsyncTranslationResource(self) + + @cached_property + def vision(self) -> AsyncVisionResource: + from .resources.vision import AsyncVisionResource + + return AsyncVisionResource(self) + + @cached_property + def with_raw_response(self) -> AsyncWriterWithRawResponse: + return AsyncWriterWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncWriterWithStreamedResponse: + return AsyncWriterWithStreamedResponse(self) @property @override @@ -408,55 +494,247 @@ def _make_status_error( class WriterWithRawResponse: + _client: Writer + def __init__(self, client: Writer) -> None: - self.applications = applications.ApplicationsResourceWithRawResponse(client.applications) - self.chat = chat.ChatResourceWithRawResponse(client.chat) - self.completions = completions.CompletionsResourceWithRawResponse(client.completions) - self.models = models.ModelsResourceWithRawResponse(client.models) - self.graphs = graphs.GraphsResourceWithRawResponse(client.graphs) - self.files = files.FilesResourceWithRawResponse(client.files) - self.tools = tools.ToolsResourceWithRawResponse(client.tools) - self.translation = translation.TranslationResourceWithRawResponse(client.translation) - self.vision = vision.VisionResourceWithRawResponse(client.vision) + self._client = client + + @cached_property + def applications(self) -> applications.ApplicationsResourceWithRawResponse: + from .resources.applications import ApplicationsResourceWithRawResponse + + return ApplicationsResourceWithRawResponse(self._client.applications) + + @cached_property + def chat(self) -> chat.ChatResourceWithRawResponse: + from .resources.chat import ChatResourceWithRawResponse + + return ChatResourceWithRawResponse(self._client.chat) + + @cached_property + def completions(self) -> completions.CompletionsResourceWithRawResponse: + from .resources.completions import CompletionsResourceWithRawResponse + + return CompletionsResourceWithRawResponse(self._client.completions) + + @cached_property + def models(self) -> models.ModelsResourceWithRawResponse: + from .resources.models import ModelsResourceWithRawResponse + + return ModelsResourceWithRawResponse(self._client.models) + + @cached_property + def graphs(self) -> graphs.GraphsResourceWithRawResponse: + from .resources.graphs import GraphsResourceWithRawResponse + + return GraphsResourceWithRawResponse(self._client.graphs) + + @cached_property + def files(self) -> files.FilesResourceWithRawResponse: + from .resources.files import FilesResourceWithRawResponse + + return FilesResourceWithRawResponse(self._client.files) + + @cached_property + def tools(self) -> tools.ToolsResourceWithRawResponse: + from .resources.tools import ToolsResourceWithRawResponse + + return ToolsResourceWithRawResponse(self._client.tools) + + @cached_property + def translation(self) -> translation.TranslationResourceWithRawResponse: + from .resources.translation import TranslationResourceWithRawResponse + + return TranslationResourceWithRawResponse(self._client.translation) + + @cached_property + def vision(self) -> vision.VisionResourceWithRawResponse: + from .resources.vision import VisionResourceWithRawResponse + + return VisionResourceWithRawResponse(self._client.vision) class AsyncWriterWithRawResponse: + _client: AsyncWriter + def __init__(self, client: AsyncWriter) -> None: - self.applications = applications.AsyncApplicationsResourceWithRawResponse(client.applications) - self.chat = chat.AsyncChatResourceWithRawResponse(client.chat) - self.completions = completions.AsyncCompletionsResourceWithRawResponse(client.completions) - self.models = models.AsyncModelsResourceWithRawResponse(client.models) - self.graphs = graphs.AsyncGraphsResourceWithRawResponse(client.graphs) - self.files = files.AsyncFilesResourceWithRawResponse(client.files) - self.tools = tools.AsyncToolsResourceWithRawResponse(client.tools) - self.translation = translation.AsyncTranslationResourceWithRawResponse(client.translation) - self.vision = vision.AsyncVisionResourceWithRawResponse(client.vision) + self._client = client + + @cached_property + def applications(self) -> applications.AsyncApplicationsResourceWithRawResponse: + from .resources.applications import AsyncApplicationsResourceWithRawResponse + + return AsyncApplicationsResourceWithRawResponse(self._client.applications) + + @cached_property + def chat(self) -> chat.AsyncChatResourceWithRawResponse: + from .resources.chat import AsyncChatResourceWithRawResponse + + return AsyncChatResourceWithRawResponse(self._client.chat) + + @cached_property + def completions(self) -> completions.AsyncCompletionsResourceWithRawResponse: + from .resources.completions import AsyncCompletionsResourceWithRawResponse + + return AsyncCompletionsResourceWithRawResponse(self._client.completions) + + @cached_property + def models(self) -> models.AsyncModelsResourceWithRawResponse: + from .resources.models import AsyncModelsResourceWithRawResponse + + return AsyncModelsResourceWithRawResponse(self._client.models) + + @cached_property + def graphs(self) -> graphs.AsyncGraphsResourceWithRawResponse: + from .resources.graphs import AsyncGraphsResourceWithRawResponse + + return AsyncGraphsResourceWithRawResponse(self._client.graphs) + + @cached_property + def files(self) -> files.AsyncFilesResourceWithRawResponse: + from .resources.files import AsyncFilesResourceWithRawResponse + + return AsyncFilesResourceWithRawResponse(self._client.files) + + @cached_property + def tools(self) -> tools.AsyncToolsResourceWithRawResponse: + from .resources.tools import AsyncToolsResourceWithRawResponse + + return AsyncToolsResourceWithRawResponse(self._client.tools) + + @cached_property + def translation(self) -> translation.AsyncTranslationResourceWithRawResponse: + from .resources.translation import AsyncTranslationResourceWithRawResponse + + return AsyncTranslationResourceWithRawResponse(self._client.translation) + + @cached_property + def vision(self) -> vision.AsyncVisionResourceWithRawResponse: + from .resources.vision import AsyncVisionResourceWithRawResponse + + return AsyncVisionResourceWithRawResponse(self._client.vision) class WriterWithStreamedResponse: + _client: Writer + def __init__(self, client: Writer) -> None: - self.applications = applications.ApplicationsResourceWithStreamingResponse(client.applications) - self.chat = chat.ChatResourceWithStreamingResponse(client.chat) - self.completions = completions.CompletionsResourceWithStreamingResponse(client.completions) - self.models = models.ModelsResourceWithStreamingResponse(client.models) - self.graphs = graphs.GraphsResourceWithStreamingResponse(client.graphs) - self.files = files.FilesResourceWithStreamingResponse(client.files) - self.tools = tools.ToolsResourceWithStreamingResponse(client.tools) - self.translation = translation.TranslationResourceWithStreamingResponse(client.translation) - self.vision = vision.VisionResourceWithStreamingResponse(client.vision) + self._client = client + + @cached_property + def applications(self) -> applications.ApplicationsResourceWithStreamingResponse: + from .resources.applications import ApplicationsResourceWithStreamingResponse + + return ApplicationsResourceWithStreamingResponse(self._client.applications) + + @cached_property + def chat(self) -> chat.ChatResourceWithStreamingResponse: + from .resources.chat import ChatResourceWithStreamingResponse + + return ChatResourceWithStreamingResponse(self._client.chat) + + @cached_property + def completions(self) -> completions.CompletionsResourceWithStreamingResponse: + from .resources.completions import CompletionsResourceWithStreamingResponse + + return CompletionsResourceWithStreamingResponse(self._client.completions) + + @cached_property + def models(self) -> models.ModelsResourceWithStreamingResponse: + from .resources.models import ModelsResourceWithStreamingResponse + + return ModelsResourceWithStreamingResponse(self._client.models) + + @cached_property + def graphs(self) -> graphs.GraphsResourceWithStreamingResponse: + from .resources.graphs import GraphsResourceWithStreamingResponse + + return GraphsResourceWithStreamingResponse(self._client.graphs) + + @cached_property + def files(self) -> files.FilesResourceWithStreamingResponse: + from .resources.files import FilesResourceWithStreamingResponse + + return FilesResourceWithStreamingResponse(self._client.files) + + @cached_property + def tools(self) -> tools.ToolsResourceWithStreamingResponse: + from .resources.tools import ToolsResourceWithStreamingResponse + + return ToolsResourceWithStreamingResponse(self._client.tools) + + @cached_property + def translation(self) -> translation.TranslationResourceWithStreamingResponse: + from .resources.translation import TranslationResourceWithStreamingResponse + + return TranslationResourceWithStreamingResponse(self._client.translation) + + @cached_property + def vision(self) -> vision.VisionResourceWithStreamingResponse: + from .resources.vision import VisionResourceWithStreamingResponse + + return VisionResourceWithStreamingResponse(self._client.vision) class AsyncWriterWithStreamedResponse: + _client: AsyncWriter + def __init__(self, client: AsyncWriter) -> None: - self.applications = applications.AsyncApplicationsResourceWithStreamingResponse(client.applications) - self.chat = chat.AsyncChatResourceWithStreamingResponse(client.chat) - self.completions = completions.AsyncCompletionsResourceWithStreamingResponse(client.completions) - self.models = models.AsyncModelsResourceWithStreamingResponse(client.models) - self.graphs = graphs.AsyncGraphsResourceWithStreamingResponse(client.graphs) - self.files = files.AsyncFilesResourceWithStreamingResponse(client.files) - self.tools = tools.AsyncToolsResourceWithStreamingResponse(client.tools) - self.translation = translation.AsyncTranslationResourceWithStreamingResponse(client.translation) - self.vision = vision.AsyncVisionResourceWithStreamingResponse(client.vision) + self._client = client + + @cached_property + def applications(self) -> applications.AsyncApplicationsResourceWithStreamingResponse: + from .resources.applications import AsyncApplicationsResourceWithStreamingResponse + + return AsyncApplicationsResourceWithStreamingResponse(self._client.applications) + + @cached_property + def chat(self) -> chat.AsyncChatResourceWithStreamingResponse: + from .resources.chat import AsyncChatResourceWithStreamingResponse + + return AsyncChatResourceWithStreamingResponse(self._client.chat) + + @cached_property + def completions(self) -> completions.AsyncCompletionsResourceWithStreamingResponse: + from .resources.completions import AsyncCompletionsResourceWithStreamingResponse + + return AsyncCompletionsResourceWithStreamingResponse(self._client.completions) + + @cached_property + def models(self) -> models.AsyncModelsResourceWithStreamingResponse: + from .resources.models import AsyncModelsResourceWithStreamingResponse + + return AsyncModelsResourceWithStreamingResponse(self._client.models) + + @cached_property + def graphs(self) -> graphs.AsyncGraphsResourceWithStreamingResponse: + from .resources.graphs import AsyncGraphsResourceWithStreamingResponse + + return AsyncGraphsResourceWithStreamingResponse(self._client.graphs) + + @cached_property + def files(self) -> files.AsyncFilesResourceWithStreamingResponse: + from .resources.files import AsyncFilesResourceWithStreamingResponse + + return AsyncFilesResourceWithStreamingResponse(self._client.files) + + @cached_property + def tools(self) -> tools.AsyncToolsResourceWithStreamingResponse: + from .resources.tools import AsyncToolsResourceWithStreamingResponse + + return AsyncToolsResourceWithStreamingResponse(self._client.tools) + + @cached_property + def translation(self) -> translation.AsyncTranslationResourceWithStreamingResponse: + from .resources.translation import AsyncTranslationResourceWithStreamingResponse + + return AsyncTranslationResourceWithStreamingResponse(self._client.translation) + + @cached_property + def vision(self) -> vision.AsyncVisionResourceWithStreamingResponse: + from .resources.vision import AsyncVisionResourceWithStreamingResponse + + return AsyncVisionResourceWithStreamingResponse(self._client.vision) Client = Writer From 25fbfec48015956b711d99f1d943ed157180c84a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 15:49:45 +0000 Subject: [PATCH 344/399] fix: use async_to_httpx_files in patch method --- src/writerai/_base_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index a0b81924..7db549a3 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -1774,7 +1774,7 @@ async def patch( options: RequestOptions = {}, ) -> ResponseT: opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + method="patch", url=path, json_data=body, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts) From f57c1560b7b2e49992af74c7d16bfdbeff7ecfea Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 18 Dec 2025 21:53:08 +0000 Subject: [PATCH 345/399] chore(internal): add `--fix` argument to lint script --- scripts/lint | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/lint b/scripts/lint index a3bd86b1..6ac647f4 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 writerai' From 89d6b4de823fa20131ae940f76d6e0f2b397c479 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 04:07:48 +0000 Subject: [PATCH 346/399] chore(internal): codegen related update --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 38b16626..a1e82cd6 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 Writer + Copyright 2026 Writer Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 72c033f63e8b2e4cd0cd9426c97ff6ee4c881069 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 20:52:24 +0000 Subject: [PATCH 347/399] docs: prominently feature MCP server setup in root SDK readmes --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 15adecc5..959880ea 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,15 @@ and offers both synchronous and asynchronous clients powered by [httpx](https:// It is generated with [Stainless](https://www.stainless.com/). +## MCP Server + +Use the Writer 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=writer-sdk-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIndyaXRlci1zZGstbWNwIl19) +[![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%22writer-sdk-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22writer-sdk-mcp%22%5D%7D) + +> Note: You may need to set environment variables in your MCP client. + ## Documentation The REST API documentation can be found on [dev.writer.com](https://dev.writer.com/api-guides/introduction). The full API of this library can be found in [api.md](api.md). From acb6eb6372e044077116ed34cf2023c82811ed2b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 18:39:47 +0000 Subject: [PATCH 348/399] feat(api): manual updates Manually adding the default_retires config to the stainless config --- .stats.yml | 2 +- src/writerai/_constants.py | 4 +-- tests/test_client.py | 52 +++++++++++++++++++------------------- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/.stats.yml b/.stats.yml index 33ea12a4..670a4321 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 33 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-ea6ec4b34f6b7fdecc564f59b2e31482eee05830bf8dc1f389461b158de1548e.yml openapi_spec_hash: ea89c1faed473908be2740efe6da255f -config_hash: 886645f89dc98f04b8931eaf02854e5f +config_hash: 781efc49d819495a2ef4aeedc8088a6a diff --git a/src/writerai/_constants.py b/src/writerai/_constants.py index eeeb0942..1ae59e29 100644 --- a/src/writerai/_constants.py +++ b/src/writerai/_constants.py @@ -10,5 +10,5 @@ DEFAULT_MAX_RETRIES = 2 DEFAULT_CONNECTION_LIMITS = httpx.Limits(max_connections=100, max_keepalive_connections=20) -INITIAL_RETRY_DELAY = 0.5 -MAX_RETRY_DELAY = 8.0 +INITIAL_RETRY_DELAY = 1.0 +MAX_RETRY_DELAY = 60.0 diff --git a/tests/test_client.py b/tests/test_client.py index 4066755f..ac4d23d2 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -715,21 +715,21 @@ class Model(BaseModel): "remaining_retries,retry_after,timeout", [ [3, "20", 20], - [3, "0", 0.5], - [3, "-10", 0.5], + [3, "0", 1], + [3, "-10", 1], [3, "60", 60], - [3, "61", 0.5], + [3, "61", 1], [3, "Fri, 29 Sep 2023 16:26:57 GMT", 20], - [3, "Fri, 29 Sep 2023 16:26:37 GMT", 0.5], - [3, "Fri, 29 Sep 2023 16:26:27 GMT", 0.5], + [3, "Fri, 29 Sep 2023 16:26:37 GMT", 1], + [3, "Fri, 29 Sep 2023 16:26:27 GMT", 1], [3, "Fri, 29 Sep 2023 16:27:37 GMT", 60], - [3, "Fri, 29 Sep 2023 16:27:38 GMT", 0.5], - [3, "99999999999999999999999999999999999", 0.5], - [3, "Zun, 29 Sep 2023 16:26:27 GMT", 0.5], - [3, "", 0.5], - [2, "", 0.5 * 2.0], - [1, "", 0.5 * 4.0], - [-1100, "", 8], # test large number potentially overflowing + [3, "Fri, 29 Sep 2023 16:27:38 GMT", 1], + [3, "99999999999999999999999999999999999", 1], + [3, "Zun, 29 Sep 2023 16:26:27 GMT", 1], + [3, "", 1], + [2, "", 1 * 2.0], + [1, "", 1 * 4.0], + [-1100, "", 60], # test large number potentially overflowing ], ) @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) @@ -739,7 +739,7 @@ def test_parse_retry_after_header( headers = httpx.Headers({"retry-after": retry_after}) options = FinalRequestOptions(method="get", url="/foo", max_retries=3) calculated = client._calculate_retry_timeout(remaining_retries, options, headers) - assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType] + assert calculated == pytest.approx(timeout, 1 * 0.875) # pyright: ignore[reportUnknownMemberType] @mock.patch("writerai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) @@ -1562,21 +1562,21 @@ class Model(BaseModel): "remaining_retries,retry_after,timeout", [ [3, "20", 20], - [3, "0", 0.5], - [3, "-10", 0.5], + [3, "0", 1], + [3, "-10", 1], [3, "60", 60], - [3, "61", 0.5], + [3, "61", 1], [3, "Fri, 29 Sep 2023 16:26:57 GMT", 20], - [3, "Fri, 29 Sep 2023 16:26:37 GMT", 0.5], - [3, "Fri, 29 Sep 2023 16:26:27 GMT", 0.5], + [3, "Fri, 29 Sep 2023 16:26:37 GMT", 1], + [3, "Fri, 29 Sep 2023 16:26:27 GMT", 1], [3, "Fri, 29 Sep 2023 16:27:37 GMT", 60], - [3, "Fri, 29 Sep 2023 16:27:38 GMT", 0.5], - [3, "99999999999999999999999999999999999", 0.5], - [3, "Zun, 29 Sep 2023 16:26:27 GMT", 0.5], - [3, "", 0.5], - [2, "", 0.5 * 2.0], - [1, "", 0.5 * 4.0], - [-1100, "", 8], # test large number potentially overflowing + [3, "Fri, 29 Sep 2023 16:27:38 GMT", 1], + [3, "99999999999999999999999999999999999", 1], + [3, "Zun, 29 Sep 2023 16:26:27 GMT", 1], + [3, "", 1], + [2, "", 1 * 2.0], + [1, "", 1 * 4.0], + [-1100, "", 60], # test large number potentially overflowing ], ) @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) @@ -1586,7 +1586,7 @@ async def test_parse_retry_after_header( headers = httpx.Headers({"retry-after": retry_after}) options = FinalRequestOptions(method="get", url="/foo", max_retries=3) calculated = async_client._calculate_retry_timeout(remaining_retries, options, headers) - assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType] + assert calculated == pytest.approx(timeout, 1 * 0.875) # pyright: ignore[reportUnknownMemberType] @mock.patch("writerai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) From ab9b149afd1da111384756173d219d654c1c8c70 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 21:36:11 +0000 Subject: [PATCH 349/399] feat(api): manual updates Manually updated max_retries param --- .stats.yml | 2 +- README.md | 2 +- src/writerai/_constants.py | 2 +- tests/test_client.py | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.stats.yml b/.stats.yml index 670a4321..a774d25b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 33 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-ea6ec4b34f6b7fdecc564f59b2e31482eee05830bf8dc1f389461b158de1548e.yml openapi_spec_hash: ea89c1faed473908be2740efe6da255f -config_hash: 781efc49d819495a2ef4aeedc8088a6a +config_hash: 247c2ce23a36ef7446d356308329c87b diff --git a/README.md b/README.md index 959880ea..b00aa245 100644 --- a/README.md +++ b/README.md @@ -314,7 +314,7 @@ Error codes are as follows: ### Retries -Certain errors are automatically retried 2 times by default, with a short exponential backoff. +Certain errors are automatically retried 7 times by default, with a short exponential backoff. Connection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict, 429 Rate Limit, and >=500 Internal errors are all retried by default. diff --git a/src/writerai/_constants.py b/src/writerai/_constants.py index 1ae59e29..9a4c97ab 100644 --- a/src/writerai/_constants.py +++ b/src/writerai/_constants.py @@ -7,7 +7,7 @@ # default timeout is 3 minutes DEFAULT_TIMEOUT = httpx.Timeout(timeout=180, connect=5.0) -DEFAULT_MAX_RETRIES = 2 +DEFAULT_MAX_RETRIES = 7 DEFAULT_CONNECTION_LIMITS = httpx.Limits(max_connections=100, max_keepalive_connections=20) INITIAL_RETRY_DELAY = 1.0 diff --git a/tests/test_client.py b/tests/test_client.py index ac4d23d2..f90d3a04 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -92,7 +92,7 @@ def test_copy_default_options(self, client: Writer) -> None: # options that have a default are overridden correctly copied = client.copy(max_retries=7) assert copied.max_retries == 7 - assert client.max_retries == 2 + assert client.max_retries == 7 copied2 = copied.copy(max_retries=6) assert copied2.max_retries == 6 @@ -922,7 +922,7 @@ def test_copy_default_options(self, async_client: AsyncWriter) -> None: # options that have a default are overridden correctly copied = async_client.copy(max_retries=7) assert copied.max_retries == 7 - assert async_client.max_retries == 2 + assert async_client.max_retries == 7 copied2 = copied.copy(max_retries=6) assert copied2.max_retries == 6 From f41a42448a4b269bdcb6363c362c99b0139bcf4a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 23:26:28 +0000 Subject: [PATCH 350/399] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 7f554478..7f1fa0b4 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.3.3-rc1" + ".": "2.4.0-rc1" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 92e69a44..ae7b1c30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "2.3.3-rc1" +version = "2.4.0-rc1" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index 6ff9a512..51018589 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "2.3.3-rc1" # x-release-please-version +__version__ = "2.4.0-rc1" # x-release-please-version From 2bb8f46c5c031e4f140ab5433e99ca4a7a3717b8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 17:05:59 +0000 Subject: [PATCH 351/399] chore(internal): codegen related update --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b00aa245..8426b957 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ The REST API documentation can be found on [dev.writer.com](https://dev.writer.c ```sh # install from PyPI -pip install --pre writer-sdk +pip install '--pre writer-sdk' ``` ## Usage @@ -98,7 +98,7 @@ You can enable this by installing `aiohttp`: ```sh # install from PyPI -pip install --pre writer-sdk[aiohttp] +pip install '--pre writer-sdk[aiohttp]' ``` Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: From b3f10a9306057b17d1cdd1f706dbfbc16d259c15 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 18:35:13 +0000 Subject: [PATCH 352/399] feat(client): add support for binary request streaming --- src/writerai/_base_client.py | 145 ++++++++++++++++++++++++--- src/writerai/_models.py | 17 +++- src/writerai/_types.py | 9 ++ tests/test_client.py | 187 ++++++++++++++++++++++++++++++++++- 4 files changed, 344 insertions(+), 14 deletions(-) diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index 7db549a3..6e45bf63 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -9,6 +9,7 @@ import inspect import logging import platform +import warnings import email.utils from types import TracebackType from random import random @@ -51,9 +52,11 @@ ResponseT, AnyMapping, PostParser, + BinaryTypes, RequestFiles, HttpxSendArgs, RequestOptions, + AsyncBinaryTypes, HttpxRequestFiles, ModelBuilderProtocol, not_given, @@ -477,8 +480,19 @@ def _build_request( retries_taken: int = 0, ) -> httpx.Request: if log.isEnabledFor(logging.DEBUG): - log.debug("Request options: %s", model_dump(options, exclude_unset=True)) - + log.debug( + "Request options: %s", + model_dump( + options, + exclude_unset=True, + # Pydantic v1 can't dump every type we support in content, so we exclude it for now. + exclude={ + "content", + } + if PYDANTIC_V1 + else {}, + ), + ) kwargs: dict[str, Any] = {} json_data = options.json_data @@ -532,7 +546,13 @@ def _build_request( is_body_allowed = options.method.lower() != "get" if is_body_allowed: - if isinstance(json_data, bytes): + if options.content is not None and json_data is not None: + raise TypeError("Passing both `content` and `json_data` is not supported") + if options.content is not None and files is not None: + raise TypeError("Passing both `content` and `files` is not supported") + if options.content is not None: + kwargs["content"] = options.content + elif isinstance(json_data, bytes): kwargs["content"] = json_data else: kwargs["json"] = json_data if is_given(json_data) else None @@ -1194,6 +1214,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: Literal[False] = False, @@ -1206,6 +1227,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: Literal[True], @@ -1219,6 +1241,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: bool, @@ -1231,13 +1254,25 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: bool = False, stream_cls: type[_StreamT] | None = None, ) -> ResponseT | _StreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=to_httpx_files(files), **options + method="post", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) @@ -1247,11 +1282,23 @@ def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + method="patch", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return self.request(cast_to, opts) @@ -1261,11 +1308,23 @@ def put( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=to_httpx_files(files), **options + method="put", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return self.request(cast_to, opts) @@ -1275,9 +1334,19 @@ def delete( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) return self.request(cast_to, opts) def get_api_list( @@ -1717,6 +1786,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: Literal[False] = False, @@ -1729,6 +1799,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: Literal[True], @@ -1742,6 +1813,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: bool, @@ -1754,13 +1826,25 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: bool = False, stream_cls: type[_AsyncStreamT] | None = None, ) -> ResponseT | _AsyncStreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="post", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) @@ -1770,11 +1854,28 @@ async def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="patch", + url=path, + json_data=body, + content=content, + files=await async_to_httpx_files(files), + **options, ) return await self.request(cast_to, opts) @@ -1784,11 +1885,23 @@ async def put( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="put", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts) @@ -1798,9 +1911,19 @@ async def delete( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) return await self.request(cast_to, opts) def get_api_list( diff --git a/src/writerai/_models.py b/src/writerai/_models.py index ca9500b2..29070e05 100644 --- a/src/writerai/_models.py +++ b/src/writerai/_models.py @@ -3,7 +3,20 @@ import os import inspect import weakref -from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast +from typing import ( + IO, + TYPE_CHECKING, + Any, + Type, + Union, + Generic, + TypeVar, + Callable, + Iterable, + Optional, + AsyncIterable, + cast, +) from datetime import date, datetime from typing_extensions import ( List, @@ -787,6 +800,7 @@ class FinalRequestOptionsInput(TypedDict, total=False): timeout: float | Timeout | None files: HttpxRequestFiles | None idempotency_key: str + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] json_data: Body extra_json: AnyMapping follow_redirects: bool @@ -805,6 +819,7 @@ class FinalRequestOptions(pydantic.BaseModel): post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() follow_redirects: Union[bool, None] = None + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] = None # It should be noted that we cannot use `json` here as that would override # a BaseModel method in an incompatible fashion. json_data: Union[Body, None] = None diff --git a/src/writerai/_types.py b/src/writerai/_types.py index ed3a7f53..44e94d72 100644 --- a/src/writerai/_types.py +++ b/src/writerai/_types.py @@ -13,9 +13,11 @@ Mapping, TypeVar, Callable, + Iterable, Iterator, Optional, Sequence, + AsyncIterable, ) from typing_extensions import ( Set, @@ -56,6 +58,13 @@ else: Base64FileInput = Union[IO[bytes], PathLike] FileContent = Union[IO[bytes], bytes, PathLike] # PathLike is not subscriptable in Python 3.8. + + +# Used for sending raw binary data / streaming data in request bodies +# e.g. for file uploads without multipart encoding +BinaryTypes = Union[bytes, bytearray, IO[bytes], Iterable[bytes]] +AsyncBinaryTypes = Union[bytes, bytearray, IO[bytes], AsyncIterable[bytes]] + FileTypes = Union[ # file (or bytes) FileContent, diff --git a/tests/test_client.py b/tests/test_client.py index f90d3a04..02659d7f 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 @@ -37,6 +38,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" @@ -51,6 +53,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: Writer | AsyncWriter) -> int: transport = client._client._transport assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport) @@ -501,6 +554,70 @@ def test_multipart_repeating_array(self, client: Writer) -> None: b"", ] + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload(self, respx_mock: MockRouter, client: Writer) -> 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 Writer( + 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: Writer) -> 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: Writer) -> None: class Model1(BaseModel): @@ -1335,6 +1452,72 @@ def test_multipart_repeating_array(self, async_client: AsyncWriter) -> None: b"", ] + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload(self, respx_mock: MockRouter, async_client: AsyncWriter) -> 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 AsyncWriter( + 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: AsyncWriter + ) -> 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: AsyncWriter) -> None: class Model1(BaseModel): From 3b1d30dabbe1b7a25bd71c3172f15db3d9128ec7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 18:33:00 +0000 Subject: [PATCH 353/399] chore(internal): update `actions/checkout` version --- .github/workflows/ci.yml | 6 +++--- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/release-doctor.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b1e32a40..71de2a88 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/writer-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/writer-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | @@ -81,7 +81,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/writer-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 85424099..a150e406 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 43d314d3..4a6bb93d 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -12,7 +12,7 @@ jobs: if: github.repository == 'writer/writer-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: | From 9014b068353204386affa1a1cdaf0c08920e0164 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 23 Jan 2026 18:16:51 +0000 Subject: [PATCH 354/399] chore(ci): upgrade `actions/github-script` --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 71de2a88..5b6ff846 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,7 +63,7 @@ jobs: - name: Get GitHub OIDC Token if: github.repository == 'stainless-sdks/writer-python' id: github-oidc - uses: actions/github-script@v6 + uses: actions/github-script@v8 with: script: core.setOutput('github_token', await core.getIDToken()); From 9e6740ebbf01bfdb2b2f82cef0d8e7a703b6f454 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 16:42:41 +0000 Subject: [PATCH 355/399] fix(docs): fix mcp installation instructions for remote servers --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8426b957..b3542e5f 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ It is generated with [Stainless](https://www.stainless.com/). Use the Writer 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=writer-sdk-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIndyaXRlci1zZGstbWNwIl19) -[![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%22writer-sdk-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22writer-sdk-mcp%22%5D%7D) +[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=writer-sdk-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIndyaXRlci1zZGstbWNwIl0sImVudiI6eyJXUklURVJfQVBJX0tFWSI6Ik15IEFQSSBLZXkifX0) +[![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%22writer-sdk-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22writer-sdk-mcp%22%5D%2C%22env%22%3A%7B%22WRITER_API_KEY%22%3A%22My%20API%20Key%22%7D%7D) > Note: You may need to set environment variables in your MCP client. From de9c1549ae5c88070d4843551194ca852b1a89bb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:57:29 +0000 Subject: [PATCH 356/399] feat(client): add custom JSON encoder for extended type support --- src/writerai/_base_client.py | 7 +- src/writerai/_compat.py | 6 +- src/writerai/_utils/_json.py | 35 ++++++++++ tests/test_utils/test_json.py | 126 ++++++++++++++++++++++++++++++++++ 4 files changed, 169 insertions(+), 5 deletions(-) create mode 100644 src/writerai/_utils/_json.py create mode 100644 tests/test_utils/test_json.py diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index 6e45bf63..62fb9f9b 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -86,6 +86,7 @@ APIConnectionError, APIResponseValidationError, ) +from ._utils._json import openapi_dumps log: logging.Logger = logging.getLogger(__name__) @@ -554,8 +555,10 @@ def _build_request( kwargs["content"] = options.content elif isinstance(json_data, bytes): kwargs["content"] = json_data - else: - kwargs["json"] = json_data if is_given(json_data) else None + elif not files: + # Don't set content when JSON is sent as multipart/form-data, + # since httpx's content param overrides other body arguments + kwargs["content"] = openapi_dumps(json_data) if is_given(json_data) and json_data is not None else None kwargs["files"] = files else: headers.pop("Content-Type", None) diff --git a/src/writerai/_compat.py b/src/writerai/_compat.py index bdef67f0..786ff42a 100644 --- a/src/writerai/_compat.py +++ b/src/writerai/_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/writerai/_utils/_json.py b/src/writerai/_utils/_json.py new file mode 100644 index 00000000..60584214 --- /dev/null +++ b/src/writerai/_utils/_json.py @@ -0,0 +1,35 @@ +import json +from typing import Any +from datetime import datetime +from typing_extensions import override + +import pydantic + +from .._compat import model_dump + + +def openapi_dumps(obj: Any) -> bytes: + """ + Serialize an object to UTF-8 encoded JSON bytes. + + Extends the standard json.dumps with support for additional types + commonly used in the SDK, such as `datetime`, `pydantic.BaseModel`, etc. + """ + return json.dumps( + obj, + cls=_CustomEncoder, + # Uses the same defaults as httpx's JSON serialization + ensure_ascii=False, + separators=(",", ":"), + allow_nan=False, + ).encode() + + +class _CustomEncoder(json.JSONEncoder): + @override + def default(self, o: Any) -> Any: + if isinstance(o, datetime): + return o.isoformat() + if isinstance(o, pydantic.BaseModel): + return model_dump(o, exclude_unset=True, mode="json", by_alias=True) + return super().default(o) diff --git a/tests/test_utils/test_json.py b/tests/test_utils/test_json.py new file mode 100644 index 00000000..9d63c901 --- /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 writerai import _compat +from writerai._utils._json import openapi_dumps + + +class TestOpenapiDumps: + def test_basic(self) -> None: + data = {"key": "value", "number": 42} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"key":"value","number":42}' + + def test_datetime_serialization(self) -> None: + dt = datetime.datetime(2023, 1, 1, 12, 0, 0) + data = {"datetime": dt} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"datetime":"2023-01-01T12:00:00"}' + + def test_pydantic_model_serialization(self) -> None: + class User(pydantic.BaseModel): + first_name: str + last_name: str + age: int + + model_instance = User(first_name="John", last_name="Kramer", age=83) + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"first_name":"John","last_name":"Kramer","age":83}}' + + def test_pydantic_model_with_default_values(self) -> None: + class User(pydantic.BaseModel): + name: str + role: str = "user" + active: bool = True + score: int = 0 + + model_instance = User(name="Alice") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Alice"}}' + + def test_pydantic_model_with_default_values_overridden(self) -> None: + class User(pydantic.BaseModel): + name: str + role: str = "user" + active: bool = True + + model_instance = User(name="Bob", role="admin", active=False) + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Bob","role":"admin","active":false}}' + + def test_pydantic_model_with_alias(self) -> None: + class User(pydantic.BaseModel): + first_name: str = pydantic.Field(alias="firstName") + last_name: str = pydantic.Field(alias="lastName") + + model_instance = User(firstName="John", lastName="Doe") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"firstName":"John","lastName":"Doe"}}' + + def test_pydantic_model_with_alias_and_default(self) -> None: + class User(pydantic.BaseModel): + user_name: str = pydantic.Field(alias="userName") + user_role: str = pydantic.Field(default="member", alias="userRole") + is_active: bool = pydantic.Field(default=True, alias="isActive") + + model_instance = User(userName="charlie") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"userName":"charlie"}}' + + model_with_overrides = User(userName="diana", userRole="admin", isActive=False) + data = {"model": model_with_overrides} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"userName":"diana","userRole":"admin","isActive":false}}' + + def test_pydantic_model_with_nested_models_and_defaults(self) -> None: + class Address(pydantic.BaseModel): + street: str + city: str = "Unknown" + + class User(pydantic.BaseModel): + name: str + address: Address + verified: bool = False + + if _compat.PYDANTIC_V1: + # to handle forward references in Pydantic v1 + User.update_forward_refs(**locals()) # type: ignore[reportDeprecated] + + address = Address(street="123 Main St") + user = User(name="Diana", address=address) + data = {"user": user} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"user":{"name":"Diana","address":{"street":"123 Main St"}}}' + + address_with_city = Address(street="456 Oak Ave", city="Boston") + user_verified = User(name="Eve", address=address_with_city, verified=True) + data = {"user": user_verified} + json_bytes = openapi_dumps(data) + assert ( + json_bytes == b'{"user":{"name":"Eve","address":{"street":"456 Oak Ave","city":"Boston"},"verified":true}}' + ) + + def test_pydantic_model_with_optional_fields(self) -> None: + class User(pydantic.BaseModel): + name: str + email: Union[str, None] + phone: Union[str, None] + + model_with_none = User(name="Eve", email=None, phone=None) + data = {"model": model_with_none} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Eve","email":null,"phone":null}}' + + model_with_values = User(name="Frank", email="frank@example.com", phone=None) + data = {"model": model_with_values} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Frank","email":"frank@example.com","phone":null}}' From 833d1c39a69ed989cfe9fbdb8bbec45959d22eb6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 17:08:09 +0000 Subject: [PATCH 357/399] chore(internal): version bump --- .release-please-manifest.json | 2 +- README.md | 4 ++-- pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 7f1fa0b4..b44b2870 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.4.0-rc1" + ".": "2.4.0" } \ No newline at end of file diff --git a/README.md b/README.md index b3542e5f..8be84e69 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ The REST API documentation can be found on [dev.writer.com](https://dev.writer.c ```sh # install from PyPI -pip install '--pre writer-sdk' +pip install writer-sdk ``` ## Usage @@ -98,7 +98,7 @@ You can enable this by installing `aiohttp`: ```sh # install from PyPI -pip install '--pre writer-sdk[aiohttp]' +pip install writer-sdk[aiohttp] ``` Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: diff --git a/pyproject.toml b/pyproject.toml index ae7b1c30..a545b9d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "2.4.0-rc1" +version = "2.4.0" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index 51018589..227300e7 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "2.4.0-rc1" # x-release-please-version +__version__ = "2.4.0" # x-release-please-version From 6afb826afe170c90b8368dc56287232f732e1092 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 14:37:09 +0000 Subject: [PATCH 358/399] chore(internal): bump dependencies --- requirements-dev.lock | 20 ++++++++++---------- requirements.lock | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/requirements-dev.lock b/requirements-dev.lock index f2bde9e2..96b012f6 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -12,14 +12,14 @@ -e file:. aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.13.2 +aiohttp==3.13.3 # via httpx-aiohttp # via writer-sdk aiosignal==1.4.0 # via aiohttp annotated-types==0.7.0 # via pydantic -anyio==4.12.0 +anyio==4.12.1 # via httpx # via writer-sdk argcomplete==3.6.3 @@ -31,7 +31,7 @@ attrs==25.4.0 # via nox backports-asyncio-runner==1.2.0 # via pytest-asyncio -certifi==2025.11.12 +certifi==2026.1.4 # via httpcore # via httpx colorlog==6.10.1 @@ -61,7 +61,7 @@ httpx==0.28.1 # via httpx-aiohttp # via respx # via writer-sdk -httpx-aiohttp==0.1.9 +httpx-aiohttp==0.1.12 # via writer-sdk humanize==4.13.0 # via nox @@ -69,7 +69,7 @@ idna==3.11 # via anyio # via httpx # via yarl -importlib-metadata==8.7.0 +importlib-metadata==8.7.1 iniconfig==2.1.0 # via pytest markdown-it-py==3.0.0 @@ -82,14 +82,14 @@ multidict==6.7.0 mypy==1.17.0 mypy-extensions==1.1.0 # via mypy -nodeenv==1.9.1 +nodeenv==1.10.0 # via pyright nox==2025.11.12 packaging==25.0 # via dependency-groups # via nox # via pytest -pathspec==0.12.1 +pathspec==1.0.3 # via mypy platformdirs==4.4.0 # via virtualenv @@ -115,13 +115,13 @@ python-dateutil==2.9.0.post0 # via time-machine respx==0.22.0 rich==14.2.0 -ruff==0.14.7 +ruff==0.14.13 six==1.17.0 # via python-dateutil sniffio==1.3.1 # via writer-sdk time-machine==2.19.0 -tomli==2.3.0 +tomli==2.4.0 # via dependency-groups # via mypy # via nox @@ -141,7 +141,7 @@ typing-extensions==4.15.0 # via writer-sdk typing-inspection==0.4.2 # via pydantic -virtualenv==20.35.4 +virtualenv==20.36.1 # via nox yarl==1.22.0 # via aiohttp diff --git a/requirements.lock b/requirements.lock index 90343d3b..da1e220e 100644 --- a/requirements.lock +++ b/requirements.lock @@ -12,21 +12,21 @@ -e file:. aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.13.2 +aiohttp==3.13.3 # via httpx-aiohttp # via writer-sdk aiosignal==1.4.0 # via aiohttp annotated-types==0.7.0 # via pydantic -anyio==4.12.0 +anyio==4.12.1 # via httpx # via writer-sdk async-timeout==5.0.1 # via aiohttp attrs==25.4.0 # via aiohttp -certifi==2025.11.12 +certifi==2026.1.4 # via httpcore # via httpx distro==1.9.0 @@ -43,7 +43,7 @@ httpcore==1.0.9 httpx==0.28.1 # via httpx-aiohttp # via writer-sdk -httpx-aiohttp==0.1.9 +httpx-aiohttp==0.1.12 # via writer-sdk idna==3.11 # via anyio From 194ea43297c7487381786221fab8f347fc25beea Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 14:26:37 +0000 Subject: [PATCH 359/399] chore(internal): fix lint error on Python 3.14 --- src/writerai/_utils/_compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/writerai/_utils/_compat.py b/src/writerai/_utils/_compat.py index dd703233..2c70b299 100644 --- a/src/writerai/_utils/_compat.py +++ b/src/writerai/_utils/_compat.py @@ -26,7 +26,7 @@ def is_union(tp: Optional[Type[Any]]) -> bool: else: import types - return tp is Union or tp is types.UnionType + return tp is Union or tp is types.UnionType # type: ignore[comparison-overlap] def is_typeddict(tp: Type[Any]) -> bool: From 4867e755cba075395051b8eda473fded866ffcf7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:58:00 +0000 Subject: [PATCH 360/399] chore: format all `api.md` files --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a545b9d5..2018890b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,7 +69,7 @@ format = { chain = [ # run formatting again to fix any inconsistencies when imports are stripped "format:ruff", ]} -"format:docs" = "python scripts/utils/ruffen-docs.py README.md api.md" +"format:docs" = "bash -c 'python scripts/utils/ruffen-docs.py README.md $(find . -type f -name api.md)'" "format:ruff" = "ruff format" "lint" = { chain = [ From 2490e6d7f3bc38cfdbd10062d6e0526423617ca0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 16:35:04 +0000 Subject: [PATCH 361/399] chore: update mock server docs --- CONTRIBUTING.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 47a8ec3e..3176a923 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -88,8 +88,7 @@ $ pip install ./path-to-wheel-file.whl 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 +$ ./scripts/mock ``` ```sh From 8c328e5de7c3b6598c1d1efe481b82a4d9cfb0a4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 13:30:39 +0000 Subject: [PATCH 362/399] chore(internal): add request options to SSE classes --- src/writerai/_response.py | 3 +++ src/writerai/_streaming.py | 11 ++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/writerai/_response.py b/src/writerai/_response.py index 2183819a..814b0419 100644 --- a/src/writerai/_response.py +++ b/src/writerai/_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/writerai/_streaming.py b/src/writerai/_streaming.py index a9e69241..389e0f31 100644 --- a/src/writerai/_streaming.py +++ b/src/writerai/_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 Writer, AsyncWriter + 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: Writer, + 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__() @@ -104,7 +107,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__( @@ -113,10 +116,12 @@ def __init__( cast_to: type[_T], response: httpx.Response, client: AsyncWriter, + options: Optional[FinalRequestOptions] = None, ) -> None: self.response = response self._cast_to = cast_to self._client = client + self._options = options self._decoder = client._make_sse_decoder() self._iterator = self.__stream__() From 2879cc22d6cb3876333e1c1f4680e02e26d164f5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 20:27:54 +0000 Subject: [PATCH 363/399] chore(internal): make `test_proxy_environment_variables` more resilient --- tests/test_client.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index 02659d7f..52b1ce68 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -961,6 +961,8 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: # Test that the proxy environment variables are set correctly monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + # Delete in case our environment has this set + monkeypatch.delenv("HTTP_PROXY", raising=False) client = DefaultHttpxClient() @@ -1882,6 +1884,8 @@ async def test_get_platform(self) -> None: async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: # Test that the proxy environment variables are set correctly monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + # Delete in case our environment has this set + monkeypatch.delenv("HTTP_PROXY", raising=False) client = DefaultAsyncHttpxClient() From 0313effb96e031ec00bde6ddcbb4c2da189ae18b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:45:11 +0000 Subject: [PATCH 364/399] chore(internal): make `test_proxy_environment_variables` more resilient to env --- tests/test_client.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 52b1ce68..b13036e5 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -961,8 +961,14 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: # Test that the proxy environment variables are set correctly monkeypatch.setenv("HTTPS_PROXY", "https://example.org") - # Delete in case our environment has this set + # Delete in case our environment has any proxy env vars set monkeypatch.delenv("HTTP_PROXY", raising=False) + monkeypatch.delenv("ALL_PROXY", raising=False) + monkeypatch.delenv("NO_PROXY", raising=False) + monkeypatch.delenv("http_proxy", raising=False) + monkeypatch.delenv("https_proxy", raising=False) + monkeypatch.delenv("all_proxy", raising=False) + monkeypatch.delenv("no_proxy", raising=False) client = DefaultHttpxClient() @@ -1884,8 +1890,14 @@ async def test_get_platform(self) -> None: async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: # Test that the proxy environment variables are set correctly monkeypatch.setenv("HTTPS_PROXY", "https://example.org") - # Delete in case our environment has this set + # Delete in case our environment has any proxy env vars set monkeypatch.delenv("HTTP_PROXY", raising=False) + monkeypatch.delenv("ALL_PROXY", raising=False) + monkeypatch.delenv("NO_PROXY", raising=False) + monkeypatch.delenv("http_proxy", raising=False) + monkeypatch.delenv("https_proxy", raising=False) + monkeypatch.delenv("all_proxy", raising=False) + monkeypatch.delenv("no_proxy", raising=False) client = DefaultAsyncHttpxClient() From 54a8d2504dae536fe7fcf8a2331c54d54194b07c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:51:38 +0000 Subject: [PATCH 365/399] chore(test): do not count install time for mock server timeout --- scripts/mock | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/scripts/mock b/scripts/mock index 0b28f6ea..bcf3b392 100755 --- a/scripts/mock +++ b/scripts/mock @@ -21,11 +21,22 @@ echo "==> Starting mock server with URL ${URL}" # Run prism mock on the given spec if [ "$1" == "--daemon" ]; then + # Pre-install the package so the download doesn't eat into the startup timeout + npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism --version + npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log & - # Wait for server to come online + # Wait for server to come online (max 30s) echo -n "Waiting for server" + attempts=0 while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do + attempts=$((attempts + 1)) + if [ "$attempts" -ge 300 ]; then + echo + echo "Timed out waiting for Prism server to start" + cat .prism.log + exit 1 + fi echo -n "." sleep 0.1 done From fb2911329b9bfc4863f5d395c2a14655d04bc59a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 7 Mar 2026 13:01:47 +0000 Subject: [PATCH 366/399] chore(ci): skip uploading artifacts on stainless-internal branches --- .github/workflows/ci.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b6ff846..b6c1f5dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,14 +61,18 @@ jobs: run: rye build - name: Get GitHub OIDC Token - if: github.repository == 'stainless-sdks/writer-python' + if: |- + github.repository == 'stainless-sdks/writer-python' && + !startsWith(github.ref, 'refs/heads/stl/') id: github-oidc uses: actions/github-script@v8 with: script: core.setOutput('github_token', await core.getIDToken()); - name: Upload tarball - if: github.repository == 'stainless-sdks/writer-python' + if: |- + github.repository == 'stainless-sdks/writer-python' && + !startsWith(github.ref, 'refs/heads/stl/') env: URL: https://pkg.stainless.com/s AUTH: ${{ steps.github-oidc.outputs.github_token }} From b05f098d3506dcfea23d4042424882d5ab96b983 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 7 Mar 2026 13:21:28 +0000 Subject: [PATCH 367/399] chore: update placeholder string --- tests/api_resources/test_files.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/api_resources/test_files.py b/tests/api_resources/test_files.py index 6f4118b4..52532517 100644 --- a/tests/api_resources/test_files.py +++ b/tests/api_resources/test_files.py @@ -233,7 +233,7 @@ def test_streaming_response_retry(self, client: Writer) -> None: @parametrize def test_method_upload(self, client: Writer) -> None: file = client.files.upload( - content=b"raw file contents", + content=b"Example data", content_disposition="Content-Disposition", ) assert_matches_type(File, file, path=["response"]) @@ -242,7 +242,7 @@ def test_method_upload(self, client: Writer) -> None: @parametrize def test_method_upload_with_all_params(self, client: Writer) -> None: file = client.files.upload( - content=b"raw file contents", + content=b"Example data", content_disposition="Content-Disposition", graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) @@ -252,7 +252,7 @@ def test_method_upload_with_all_params(self, client: Writer) -> None: @parametrize def test_raw_response_upload(self, client: Writer) -> None: response = client.files.with_raw_response.upload( - content=b"raw file contents", + content=b"Example data", content_disposition="Content-Disposition", ) @@ -265,7 +265,7 @@ def test_raw_response_upload(self, client: Writer) -> None: @parametrize def test_streaming_response_upload(self, client: Writer) -> None: with client.files.with_streaming_response.upload( - content=b"raw file contents", + content=b"Example data", content_disposition="Content-Disposition", ) as response: assert not response.is_closed @@ -485,7 +485,7 @@ async def test_streaming_response_retry(self, async_client: AsyncWriter) -> None @parametrize async def test_method_upload(self, async_client: AsyncWriter) -> None: file = await async_client.files.upload( - content=b"raw file contents", + content=b"Example data", content_disposition="Content-Disposition", ) assert_matches_type(File, file, path=["response"]) @@ -494,7 +494,7 @@ async def test_method_upload(self, async_client: AsyncWriter) -> None: @parametrize async def test_method_upload_with_all_params(self, async_client: AsyncWriter) -> None: file = await async_client.files.upload( - content=b"raw file contents", + content=b"Example data", content_disposition="Content-Disposition", graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) @@ -504,7 +504,7 @@ async def test_method_upload_with_all_params(self, async_client: AsyncWriter) -> @parametrize async def test_raw_response_upload(self, async_client: AsyncWriter) -> None: response = await async_client.files.with_raw_response.upload( - content=b"raw file contents", + content=b"Example data", content_disposition="Content-Disposition", ) @@ -517,7 +517,7 @@ async def test_raw_response_upload(self, async_client: AsyncWriter) -> None: @parametrize async def test_streaming_response_upload(self, async_client: AsyncWriter) -> None: async with async_client.files.with_streaming_response.upload( - content=b"raw file contents", + content=b"Example data", content_disposition="Content-Disposition", ) as response: assert not response.is_closed From 3722f16b08eb79bff6c653ad6d8abe3b5b6edd20 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 17:39:08 +0000 Subject: [PATCH 368/399] fix(pydantic): do not pass `by_alias` unless set --- src/writerai/_compat.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/writerai/_compat.py b/src/writerai/_compat.py index 786ff42a..e6690a4f 100644 --- a/src/writerai/_compat.py +++ b/src/writerai/_compat.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload from datetime import date, datetime -from typing_extensions import Self, Literal +from typing_extensions import Self, Literal, TypedDict import pydantic from pydantic.fields import FieldInfo @@ -131,6 +131,10 @@ def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str: return model.model_dump_json(indent=indent) +class _ModelDumpKwargs(TypedDict, total=False): + by_alias: bool + + def model_dump( model: pydantic.BaseModel, *, @@ -142,6 +146,9 @@ def model_dump( by_alias: bool | None = None, ) -> dict[str, Any]: if (not PYDANTIC_V1) or hasattr(model, "model_dump"): + kwargs: _ModelDumpKwargs = {} + if by_alias is not None: + kwargs["by_alias"] = by_alias return model.model_dump( mode=mode, exclude=exclude, @@ -149,7 +156,7 @@ def model_dump( exclude_defaults=exclude_defaults, # warnings are not supported in Pydantic v1 warnings=True if PYDANTIC_V1 else warnings, - by_alias=by_alias, + **kwargs, ) return cast( "dict[str, Any]", From 5bff27d8e4747ec0e916e30c7259cf03e9e538e6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 19:08:59 +0000 Subject: [PATCH 369/399] fix(deps): bump minimum typing-extensions version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2018890b..5bcf8651 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ authors = [ dependencies = [ "httpx>=0.23.0, <1", "pydantic>=1.9.0, <3", - "typing-extensions>=4.10, <5", + "typing-extensions>=4.14, <5", "anyio>=3.5.0, <5", "distro>=1.7.0, <2", "sniffio", From 2331e6c0e447f3f297b7e7ba7fadc047ad47b95c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 20:52:49 +0000 Subject: [PATCH 370/399] chore(internal): tweak CI branches --- .github/workflows/ci.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b6c1f5dc..3d05070e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,14 @@ name: CI on: push: - branches-ignore: - - 'generated' - - 'codegen/**' - - 'integrated/**' - - 'stl-preview-head/**' - - 'stl-preview-base/**' + branches: + - '**' + - '!integrated/**' + - '!stl-preview-head/**' + - '!stl-preview-base/**' + - '!generated' + - '!codegen/**' + - 'codegen/stl/**' pull_request: branches-ignore: - 'stl-preview-head/**' From af7b6daa7340b212b2c5503c94ff1ce46044c0ef Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:43:47 +0000 Subject: [PATCH 371/399] fix: sanitize endpoint path params --- src/writerai/_utils/__init__.py | 1 + src/writerai/_utils/_path.py | 127 ++++++++++++++++++ .../resources/applications/applications.py | 10 +- src/writerai/resources/applications/graphs.py | 10 +- src/writerai/resources/applications/jobs.py | 18 +-- src/writerai/resources/files.py | 14 +- src/writerai/resources/graphs.py | 22 +-- src/writerai/resources/tools/tools.py | 6 +- tests/test_utils/test_path.py | 89 ++++++++++++ 9 files changed, 257 insertions(+), 40 deletions(-) create mode 100644 src/writerai/_utils/_path.py create mode 100644 tests/test_utils/test_path.py diff --git a/src/writerai/_utils/__init__.py b/src/writerai/_utils/__init__.py index dc64e29a..10cb66d2 100644 --- a/src/writerai/_utils/__init__.py +++ b/src/writerai/_utils/__init__.py @@ -1,3 +1,4 @@ +from ._path import path_template as path_template from ._sync import asyncify as asyncify from ._proxy import LazyProxy as LazyProxy from ._utils import ( diff --git a/src/writerai/_utils/_path.py b/src/writerai/_utils/_path.py new file mode 100644 index 00000000..4d6e1e4c --- /dev/null +++ b/src/writerai/_utils/_path.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +import re +from typing import ( + Any, + Mapping, + Callable, +) +from urllib.parse import quote + +# Matches '.' or '..' where each dot is either literal or percent-encoded (%2e / %2E). +_DOT_SEGMENT_RE = re.compile(r"^(?:\.|%2[eE]){1,2}$") + +_PLACEHOLDER_RE = re.compile(r"\{(\w+)\}") + + +def _quote_path_segment_part(value: str) -> str: + """Percent-encode `value` for use in a URI path segment. + + Considers characters not in `pchar` set from RFC 3986 §3.3 to be unsafe. + https://datatracker.ietf.org/doc/html/rfc3986#section-3.3 + """ + # quote() already treats unreserved characters (letters, digits, and -._~) + # as safe, so we only need to add sub-delims, ':', and '@'. + # Notably, unlike the default `safe` for quote(), / is unsafe and must be quoted. + return quote(value, safe="!$&'()*+,;=:@") + + +def _quote_query_part(value: str) -> str: + """Percent-encode `value` for use in a URI query string. + + Considers &, = and characters not in `query` set from RFC 3986 §3.4 to be unsafe. + https://datatracker.ietf.org/doc/html/rfc3986#section-3.4 + """ + return quote(value, safe="!$'()*+,;:@/?") + + +def _quote_fragment_part(value: str) -> str: + """Percent-encode `value` for use in a URI fragment. + + Considers characters not in `fragment` set from RFC 3986 §3.5 to be unsafe. + https://datatracker.ietf.org/doc/html/rfc3986#section-3.5 + """ + return quote(value, safe="!$&'()*+,;=:@/?") + + +def _interpolate( + template: str, + values: Mapping[str, Any], + quoter: Callable[[str], str], +) -> str: + """Replace {name} placeholders in `template`, quoting each value with `quoter`. + + Placeholder names are looked up in `values`. + + Raises: + KeyError: If a placeholder is not found in `values`. + """ + # re.split with a capturing group returns alternating + # [text, name, text, name, ..., text] elements. + parts = _PLACEHOLDER_RE.split(template) + + for i in range(1, len(parts), 2): + name = parts[i] + if name not in values: + raise KeyError(f"a value for placeholder {{{name}}} was not provided") + val = values[name] + if val is None: + parts[i] = "null" + elif isinstance(val, bool): + parts[i] = "true" if val else "false" + else: + parts[i] = quoter(str(values[name])) + + return "".join(parts) + + +def path_template(template: str, /, **kwargs: Any) -> str: + """Interpolate {name} placeholders in `template` from keyword arguments. + + Args: + template: The template string containing {name} placeholders. + **kwargs: Keyword arguments to interpolate into the template. + + Returns: + The template with placeholders interpolated and percent-encoded. + + Safe characters for percent-encoding are dependent on the URI component. + Placeholders in path and fragment portions are percent-encoded where the `segment` + and `fragment` sets from RFC 3986 respectively are considered safe. + Placeholders in the query portion are percent-encoded where the `query` set from + RFC 3986 §3.3 is considered safe except for = and & characters. + + Raises: + KeyError: If a placeholder is not found in `kwargs`. + ValueError: If resulting path contains /./ or /../ segments (including percent-encoded dot-segments). + """ + # Split the template into path, query, and fragment portions. + fragment_template: str | None = None + query_template: str | None = None + + rest = template + if "#" in rest: + rest, fragment_template = rest.split("#", 1) + if "?" in rest: + rest, query_template = rest.split("?", 1) + path_template = rest + + # Interpolate each portion with the appropriate quoting rules. + path_result = _interpolate(path_template, kwargs, _quote_path_segment_part) + + # Reject dot-segments (. and ..) in the final assembled path. The check + # runs after interpolation so that adjacent placeholders or a mix of static + # text and placeholders that together form a dot-segment are caught. + # Also reject percent-encoded dot-segments to protect against incorrectly + # implemented normalization in servers/proxies. + for segment in path_result.split("/"): + if _DOT_SEGMENT_RE.match(segment): + raise ValueError(f"Constructed path {path_result!r} contains dot-segment {segment!r} which is not allowed") + + result = path_result + if query_template is not None: + result += "?" + _interpolate(query_template, kwargs, _quote_query_part) + if fragment_template is not None: + result += "#" + _interpolate(fragment_template, kwargs, _quote_fragment_part) + + return result diff --git a/src/writerai/resources/applications/applications.py b/src/writerai/resources/applications/applications.py index d58d476f..8c13bb40 100644 --- a/src/writerai/resources/applications/applications.py +++ b/src/writerai/resources/applications/applications.py @@ -25,7 +25,7 @@ ) from ...types import application_list_params, application_generate_content_params from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from ..._utils import required_args, maybe_transform, async_maybe_transform +from ..._utils import path_template, required_args, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -100,7 +100,7 @@ def retrieve( if not application_id: raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") return self._get( - f"/v1/applications/{application_id}", + path_template("/v1/applications/{application_id}", application_id=application_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -280,7 +280,7 @@ def generate_content( if not application_id: raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") return self._post( - f"/v1/applications/{application_id}", + path_template("/v1/applications/{application_id}", application_id=application_id), body=maybe_transform( { "inputs": inputs, @@ -354,7 +354,7 @@ async def retrieve( if not application_id: raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") return await self._get( - f"/v1/applications/{application_id}", + path_template("/v1/applications/{application_id}", application_id=application_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -534,7 +534,7 @@ async def generate_content( if not application_id: raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") return await self._post( - f"/v1/applications/{application_id}", + path_template("/v1/applications/{application_id}", application_id=application_id), body=await async_maybe_transform( { "inputs": inputs, diff --git a/src/writerai/resources/applications/graphs.py b/src/writerai/resources/applications/graphs.py index 2ef559a4..212844a9 100644 --- a/src/writerai/resources/applications/graphs.py +++ b/src/writerai/resources/applications/graphs.py @@ -5,7 +5,7 @@ import httpx from ..._types import Body, Query, Headers, NotGiven, SequenceNotStr, not_given -from ..._utils import maybe_transform, async_maybe_transform +from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -72,7 +72,7 @@ def update( if not application_id: raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") return self._put( - f"/v1/applications/{application_id}/graphs", + path_template("/v1/applications/{application_id}/graphs", application_id=application_id), body=maybe_transform({"graph_ids": graph_ids}, graph_update_params.GraphUpdateParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -107,7 +107,7 @@ def list( if not application_id: raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") return self._get( - f"/v1/applications/{application_id}/graphs", + path_template("/v1/applications/{application_id}/graphs", application_id=application_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -166,7 +166,7 @@ async def update( if not application_id: raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") return await self._put( - f"/v1/applications/{application_id}/graphs", + path_template("/v1/applications/{application_id}/graphs", application_id=application_id), body=await async_maybe_transform({"graph_ids": graph_ids}, graph_update_params.GraphUpdateParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -201,7 +201,7 @@ async def list( if not application_id: raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") return await self._get( - f"/v1/applications/{application_id}/graphs", + path_template("/v1/applications/{application_id}/graphs", application_id=application_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/writerai/resources/applications/jobs.py b/src/writerai/resources/applications/jobs.py index f27f6a3e..69891e71 100644 --- a/src/writerai/resources/applications/jobs.py +++ b/src/writerai/resources/applications/jobs.py @@ -8,7 +8,7 @@ import httpx from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from ..._utils import maybe_transform, async_maybe_transform +from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -77,7 +77,7 @@ def create( if not application_id: raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") return self._post( - f"/v1/applications/{application_id}/jobs", + path_template("/v1/applications/{application_id}/jobs", application_id=application_id), body=maybe_transform({"inputs": inputs}, job_create_params.JobCreateParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -111,7 +111,7 @@ def retrieve( if not job_id: raise ValueError(f"Expected a non-empty value for `job_id` but received {job_id!r}") return self._get( - f"/v1/applications/jobs/{job_id}", + path_template("/v1/applications/jobs/{job_id}", job_id=job_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -154,7 +154,7 @@ def list( if not application_id: raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") return self._get_api_list( - f"/v1/applications/{application_id}/jobs", + path_template("/v1/applications/{application_id}/jobs", application_id=application_id), page=SyncApplicationJobsOffset[ApplicationGenerateAsyncResponse], options=make_request_options( extra_headers=extra_headers, @@ -200,7 +200,7 @@ def retry( if not job_id: raise ValueError(f"Expected a non-empty value for `job_id` but received {job_id!r}") return self._post( - f"/v1/applications/jobs/{job_id}/retry", + path_template("/v1/applications/jobs/{job_id}/retry", job_id=job_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -258,7 +258,7 @@ async def create( if not application_id: raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") return await self._post( - f"/v1/applications/{application_id}/jobs", + path_template("/v1/applications/{application_id}/jobs", application_id=application_id), body=await async_maybe_transform({"inputs": inputs}, job_create_params.JobCreateParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -292,7 +292,7 @@ async def retrieve( if not job_id: raise ValueError(f"Expected a non-empty value for `job_id` but received {job_id!r}") return await self._get( - f"/v1/applications/jobs/{job_id}", + path_template("/v1/applications/jobs/{job_id}", job_id=job_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -335,7 +335,7 @@ def list( if not application_id: raise ValueError(f"Expected a non-empty value for `application_id` but received {application_id!r}") return self._get_api_list( - f"/v1/applications/{application_id}/jobs", + path_template("/v1/applications/{application_id}/jobs", application_id=application_id), page=AsyncApplicationJobsOffset[ApplicationGenerateAsyncResponse], options=make_request_options( extra_headers=extra_headers, @@ -381,7 +381,7 @@ async def retry( if not job_id: raise ValueError(f"Expected a non-empty value for `job_id` but received {job_id!r}") return await self._post( - f"/v1/applications/jobs/{job_id}/retry", + path_template("/v1/applications/jobs/{job_id}/retry", job_id=job_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/writerai/resources/files.py b/src/writerai/resources/files.py index 5a5599c7..0741a674 100644 --- a/src/writerai/resources/files.py +++ b/src/writerai/resources/files.py @@ -8,7 +8,7 @@ from ..types import file_list_params, file_retry_params, file_upload_params from .._types import Body, Omit, Query, Headers, NotGiven, FileTypes, SequenceNotStr, omit, not_given -from .._utils import maybe_transform, async_maybe_transform +from .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -81,7 +81,7 @@ def retrieve( if not file_id: raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") return self._get( - f"/v1/files/{file_id}", + path_template("/v1/files/{file_id}", file_id=file_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -189,7 +189,7 @@ def delete( if not file_id: raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") return self._delete( - f"/v1/files/{file_id}", + path_template("/v1/files/{file_id}", file_id=file_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -225,7 +225,7 @@ def download( raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} return self._get( - f"/v1/files/{file_id}/download", + path_template("/v1/files/{file_id}/download", file_id=file_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -364,7 +364,7 @@ async def retrieve( if not file_id: raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") return await self._get( - f"/v1/files/{file_id}", + path_template("/v1/files/{file_id}", file_id=file_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -472,7 +472,7 @@ async def delete( if not file_id: raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") return await self._delete( - f"/v1/files/{file_id}", + path_template("/v1/files/{file_id}", file_id=file_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -508,7 +508,7 @@ async def download( raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} return await self._get( - f"/v1/files/{file_id}/download", + path_template("/v1/files/{file_id}/download", file_id=file_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/writerai/resources/graphs.py b/src/writerai/resources/graphs.py index fe13b4b2..1a78bcb2 100644 --- a/src/writerai/resources/graphs.py +++ b/src/writerai/resources/graphs.py @@ -15,7 +15,7 @@ graph_add_file_to_graph_params, ) from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given -from .._utils import required_args, maybe_transform, async_maybe_transform +from .._utils import path_template, required_args, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -130,7 +130,7 @@ def retrieve( if not graph_id: raise ValueError(f"Expected a non-empty value for `graph_id` but received {graph_id!r}") return self._get( - f"/v1/graphs/{graph_id}", + path_template("/v1/graphs/{graph_id}", graph_id=graph_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -176,7 +176,7 @@ def update( if not graph_id: raise ValueError(f"Expected a non-empty value for `graph_id` but received {graph_id!r}") return self._put( - f"/v1/graphs/{graph_id}", + path_template("/v1/graphs/{graph_id}", graph_id=graph_id), body=maybe_transform( { "description": description, @@ -276,7 +276,7 @@ def delete( if not graph_id: raise ValueError(f"Expected a non-empty value for `graph_id` but received {graph_id!r}") return self._delete( - f"/v1/graphs/{graph_id}", + path_template("/v1/graphs/{graph_id}", graph_id=graph_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -312,7 +312,7 @@ def add_file_to_graph( if not graph_id: raise ValueError(f"Expected a non-empty value for `graph_id` but received {graph_id!r}") return self._post( - f"/v1/graphs/{graph_id}/file", + path_template("/v1/graphs/{graph_id}/file", graph_id=graph_id), body=maybe_transform({"file_id": file_id}, graph_add_file_to_graph_params.GraphAddFileToGraphParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -516,7 +516,7 @@ def remove_file_from_graph( if not file_id: raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") return self._delete( - f"/v1/graphs/{graph_id}/file/{file_id}", + path_template("/v1/graphs/{graph_id}/file/{file_id}", graph_id=graph_id, file_id=file_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -615,7 +615,7 @@ async def retrieve( if not graph_id: raise ValueError(f"Expected a non-empty value for `graph_id` but received {graph_id!r}") return await self._get( - f"/v1/graphs/{graph_id}", + path_template("/v1/graphs/{graph_id}", graph_id=graph_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -661,7 +661,7 @@ async def update( if not graph_id: raise ValueError(f"Expected a non-empty value for `graph_id` but received {graph_id!r}") return await self._put( - f"/v1/graphs/{graph_id}", + path_template("/v1/graphs/{graph_id}", graph_id=graph_id), body=await async_maybe_transform( { "description": description, @@ -761,7 +761,7 @@ async def delete( if not graph_id: raise ValueError(f"Expected a non-empty value for `graph_id` but received {graph_id!r}") return await self._delete( - f"/v1/graphs/{graph_id}", + path_template("/v1/graphs/{graph_id}", graph_id=graph_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -797,7 +797,7 @@ async def add_file_to_graph( if not graph_id: raise ValueError(f"Expected a non-empty value for `graph_id` but received {graph_id!r}") return await self._post( - f"/v1/graphs/{graph_id}/file", + path_template("/v1/graphs/{graph_id}/file", graph_id=graph_id), body=await async_maybe_transform( {"file_id": file_id}, graph_add_file_to_graph_params.GraphAddFileToGraphParams ), @@ -1003,7 +1003,7 @@ async def remove_file_from_graph( if not file_id: raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") return await self._delete( - f"/v1/graphs/{graph_id}/file/{file_id}", + path_template("/v1/graphs/{graph_id}/file/{file_id}", graph_id=graph_id, file_id=file_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/writerai/resources/tools/tools.py b/src/writerai/resources/tools/tools.py index d48109e0..c74050d6 100644 --- a/src/writerai/resources/tools/tools.py +++ b/src/writerai/resources/tools/tools.py @@ -15,7 +15,7 @@ tool_context_aware_splitting_params, ) from ..._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given -from ..._utils import maybe_transform, async_maybe_transform +from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from .comprehend import ( ComprehendResource, @@ -186,7 +186,7 @@ def parse_pdf( if not file_id: raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") return self._post( - f"/v1/tools/pdf-parser/{file_id}", + path_template("/v1/tools/pdf-parser/{file_id}", file_id=file_id), body=maybe_transform({"format": format}, tool_parse_pdf_params.ToolParsePdfParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -616,7 +616,7 @@ async def parse_pdf( if not file_id: raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") return await self._post( - f"/v1/tools/pdf-parser/{file_id}", + path_template("/v1/tools/pdf-parser/{file_id}", file_id=file_id), body=await async_maybe_transform({"format": format}, tool_parse_pdf_params.ToolParsePdfParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout diff --git a/tests/test_utils/test_path.py b/tests/test_utils/test_path.py new file mode 100644 index 00000000..b42e3d87 --- /dev/null +++ b/tests/test_utils/test_path.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +from typing import Any + +import pytest + +from writerai._utils._path import path_template + + +@pytest.mark.parametrize( + "template, kwargs, expected", + [ + ("/v1/{id}", dict(id="abc"), "/v1/abc"), + ("/v1/{a}/{b}", dict(a="x", b="y"), "/v1/x/y"), + ("/v1/{a}{b}/path/{c}?val={d}#{e}", dict(a="x", b="y", c="z", d="u", e="v"), "/v1/xy/path/z?val=u#v"), + ("/{w}/{w}", dict(w="echo"), "/echo/echo"), + ("/v1/static", {}, "/v1/static"), + ("", {}, ""), + ("/v1/?q={n}&count=10", dict(n=42), "/v1/?q=42&count=10"), + ("/v1/{v}", dict(v=None), "/v1/null"), + ("/v1/{v}", dict(v=True), "/v1/true"), + ("/v1/{v}", dict(v=False), "/v1/false"), + ("/v1/{v}", dict(v=".hidden"), "/v1/.hidden"), # dot prefix ok + ("/v1/{v}", dict(v="file.txt"), "/v1/file.txt"), # dot in middle ok + ("/v1/{v}", dict(v="..."), "/v1/..."), # triple dot ok + ("/v1/{a}{b}", dict(a=".", b="txt"), "/v1/.txt"), # dot var combining with adjacent to be ok + ("/items?q={v}#{f}", dict(v=".", f=".."), "/items?q=.#.."), # dots in query/fragment are fine + ( + "/v1/{a}?query={b}", + dict(a="../../other/endpoint", b="a&bad=true"), + "/v1/..%2F..%2Fother%2Fendpoint?query=a%26bad%3Dtrue", + ), + ("/v1/{val}", dict(val="a/b/c"), "/v1/a%2Fb%2Fc"), + ("/v1/{val}", dict(val="a/b/c?query=value"), "/v1/a%2Fb%2Fc%3Fquery=value"), + ("/v1/{val}", dict(val="a/b/c?query=value&bad=true"), "/v1/a%2Fb%2Fc%3Fquery=value&bad=true"), + ("/v1/{val}", dict(val="%20"), "/v1/%2520"), # escapes escape sequences in input + # Query: slash and ? are safe, # is not + ("/items?q={v}", dict(v="a/b"), "/items?q=a/b"), + ("/items?q={v}", dict(v="a?b"), "/items?q=a?b"), + ("/items?q={v}", dict(v="a#b"), "/items?q=a%23b"), + ("/items?q={v}", dict(v="a b"), "/items?q=a%20b"), + # Fragment: slash and ? are safe + ("/docs#{v}", dict(v="a/b"), "/docs#a/b"), + ("/docs#{v}", dict(v="a?b"), "/docs#a?b"), + # Path: slash, ? and # are all encoded + ("/v1/{v}", dict(v="a/b"), "/v1/a%2Fb"), + ("/v1/{v}", dict(v="a?b"), "/v1/a%3Fb"), + ("/v1/{v}", dict(v="a#b"), "/v1/a%23b"), + # same var encoded differently by component + ( + "/v1/{v}?q={v}#{v}", + dict(v="a/b?c#d"), + "/v1/a%2Fb%3Fc%23d?q=a/b?c%23d#a/b?c%23d", + ), + ("/v1/{val}", dict(val="x?admin=true"), "/v1/x%3Fadmin=true"), # query injection + ("/v1/{val}", dict(val="x#admin"), "/v1/x%23admin"), # fragment injection + ], +) +def test_interpolation(template: str, kwargs: dict[str, Any], expected: str) -> None: + assert path_template(template, **kwargs) == expected + + +def test_missing_kwarg_raises_key_error() -> None: + with pytest.raises(KeyError, match="org_id"): + path_template("/v1/{org_id}") + + +@pytest.mark.parametrize( + "template, kwargs", + [ + ("{a}/path", dict(a=".")), + ("{a}/path", dict(a="..")), + ("/v1/{a}", dict(a=".")), + ("/v1/{a}", dict(a="..")), + ("/v1/{a}/path", dict(a=".")), + ("/v1/{a}/path", dict(a="..")), + ("/v1/{a}{b}", dict(a=".", b=".")), # adjacent vars → ".." + ("/v1/{a}.", dict(a=".")), # var + static → ".." + ("/v1/{a}{b}", dict(a="", b=".")), # empty + dot → "." + ("/v1/%2e/{x}", dict(x="ok")), # encoded dot in static text + ("/v1/%2e./{x}", dict(x="ok")), # mixed encoded ".." in static + ("/v1/.%2E/{x}", dict(x="ok")), # mixed encoded ".." in static + ("/v1/{v}?q=1", dict(v="..")), + ("/v1/{v}#frag", dict(v="..")), + ], +) +def test_dot_segment_rejected(template: str, kwargs: dict[str, Any]) -> None: + with pytest.raises(ValueError, match="dot-segment"): + path_template(template, **kwargs) From 93c947fa2d2f988badbb930a50b74f12ccbf28d4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 19:05:55 +0000 Subject: [PATCH 372/399] refactor(tests): switch from prism to steady --- CONTRIBUTING.md | 2 +- scripts/mock | 26 +++++++++++++------------- scripts/test | 16 ++++++++-------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3176a923..508bbc40 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -85,7 +85,7 @@ $ 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. +Most tests require you to [set up a mock server](https://github.com/dgellow/steady) against the OpenAPI spec to run the tests. ```sh $ ./scripts/mock diff --git a/scripts/mock b/scripts/mock index bcf3b392..38201de8 100755 --- a/scripts/mock +++ b/scripts/mock @@ -19,34 +19,34 @@ fi echo "==> Starting mock server with URL ${URL}" -# Run prism mock on the given spec +# Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism --version + npm exec --package=@stdy/cli@0.19.3 -- steady --version - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log & + npm exec --package=@stdy/cli@0.19.3 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-query-object-format=brackets "$URL" &> .stdy.log & - # Wait for server to come online (max 30s) + # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" attempts=0 - while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do + while ! curl --silent --fail "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1; do + if ! kill -0 $! 2>/dev/null; then + echo + cat .stdy.log + exit 1 + fi attempts=$((attempts + 1)) if [ "$attempts" -ge 300 ]; then echo - echo "Timed out waiting for Prism server to start" - cat .prism.log + echo "Timed out waiting for Steady server to start" + cat .stdy.log exit 1 fi echo -n "." sleep 0.1 done - if grep -q "✖ fatal" ".prism.log"; then - cat .prism.log - exit 1 - fi - echo else - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" + npm exec --package=@stdy/cli@0.19.3 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-query-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index dbeda2d2..2dfdc409 100755 --- a/scripts/test +++ b/scripts/test @@ -9,8 +9,8 @@ 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 +function steady_is_running() { + curl --silent "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1 } kill_server_on_port() { @@ -25,7 +25,7 @@ function is_overriding_api_base_url() { [ -n "$TEST_API_BASE_URL" ] } -if ! is_overriding_api_base_url && ! prism_is_running ; then +if ! is_overriding_api_base_url && ! steady_is_running ; then # When we exit this script, make sure to kill the background mock server process trap 'kill_server_on_port 4010' EXIT @@ -36,19 +36,19 @@ 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" +elif ! steady_is_running ; then + echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Steady 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 -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock path/to/your.openapi.yml${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.3 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-query-object-format=brackets${NC}" echo exit 1 else - echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}" + echo -e "${GREEN}✔ Mock steady server is running with your OpenAPI spec${NC}" echo fi From db20433778b13c5e463bd929598bf840bdca2a9d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 17:56:25 +0000 Subject: [PATCH 373/399] chore(tests): bump steady to v0.19.4 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 38201de8..e1c19e88 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.19.3 -- steady --version + npm exec --package=@stdy/cli@0.19.4 -- steady --version - npm exec --package=@stdy/cli@0.19.3 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-query-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.19.4 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.19.3 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-query-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.19.4 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index 2dfdc409..36fab0ae 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.3 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-query-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.4 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" echo exit 1 From 14ca9bf208df42a668c97569d8925e18a9673701 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 21:49:30 +0000 Subject: [PATCH 374/399] chore(tests): bump steady to v0.19.5 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index e1c19e88..ab814d38 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.19.4 -- steady --version + npm exec --package=@stdy/cli@0.19.5 -- steady --version - npm exec --package=@stdy/cli@0.19.4 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.19.5 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.19.4 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.19.5 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index 36fab0ae..d1c8e1a9 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.4 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.5 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" echo exit 1 From d630fd24e157608be22f29a1f29a449c4fa4efb5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 12:44:33 +0000 Subject: [PATCH 375/399] chore(internal): update gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 95ceb189..3824f4c4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .prism.log +.stdy.log _dev __pycache__ From effafd0f04f377897e87afd1116f1048a3a2cedf Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 18:05:43 +0000 Subject: [PATCH 376/399] chore(tests): bump steady to v0.19.6 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index ab814d38..b319bdfb 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.19.5 -- steady --version + npm exec --package=@stdy/cli@0.19.6 -- steady --version - npm exec --package=@stdy/cli@0.19.5 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.19.6 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.19.5 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.19.6 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index d1c8e1a9..ab01948b 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.5 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.6 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" echo exit 1 From 6f5b4ee397e83daaaa83b07b3e4927d31a64e438 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:23:05 +0000 Subject: [PATCH 377/399] chore(ci): skip lint on metadata-only changes Note that we still want to run tests, as these depend on the metadata. --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d05070e..dd0e630e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: timeout-minutes: 10 name: lint runs-on: ${{ github.repository == 'stainless-sdks/writer-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} - if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - uses: actions/checkout@v6 @@ -38,7 +38,7 @@ jobs: run: ./scripts/lint build: - if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') timeout-minutes: 10 name: build permissions: From 2da2e4d0538b7ce511a62709b1a5730f74dfdefa Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:49:21 +0000 Subject: [PATCH 378/399] chore(tests): bump steady to v0.19.7 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index b319bdfb..09eb49f6 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.19.6 -- steady --version + npm exec --package=@stdy/cli@0.19.7 -- steady --version - npm exec --package=@stdy/cli@0.19.6 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.19.6 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index ab01948b..e46b9b58 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.6 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.7 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" echo exit 1 From 067cb5d8fa7b69fda1e74876f2ccd28c49cfa5f0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 19:30:12 +0000 Subject: [PATCH 379/399] feat(internal): implement indices array format for query and form serialization --- scripts/mock | 4 ++-- scripts/test | 2 +- src/writerai/_qs.py | 5 ++++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 09eb49f6..290e21b9 100755 --- a/scripts/mock +++ b/scripts/mock @@ -24,7 +24,7 @@ if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout npm exec --package=@stdy/cli@0.19.7 -- steady --version - npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index e46b9b58..661f9bf4 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.7 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.7 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" echo exit 1 diff --git a/src/writerai/_qs.py b/src/writerai/_qs.py index ada6fd3f..de8c99bc 100644 --- a/src/writerai/_qs.py +++ b/src/writerai/_qs.py @@ -101,7 +101,10 @@ def _stringify_item( items.extend(self._stringify_item(key, item, opts)) return items elif array_format == "indices": - raise NotImplementedError("The array indices format is not supported yet") + items = [] + for i, item in enumerate(value): + items.extend(self._stringify_item(f"{key}[{i}]", item, opts)) + return items elif array_format == "brackets": items = [] key = key + "[]" From 1c55d05dc0973c5494f6d00c1ecde98490f8cca8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 13:33:38 +0000 Subject: [PATCH 380/399] chore(tests): bump steady to v0.20.1 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 290e21b9..15c29941 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.19.7 -- steady --version + npm exec --package=@stdy/cli@0.20.1 -- steady --version - npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.20.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.20.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index 661f9bf4..c8e2e9d5 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.7 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.20.1 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" echo exit 1 From f626f1abb42d1c81cd81e6288f920ebbdd276196 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 19:38:55 +0000 Subject: [PATCH 381/399] chore(tests): bump steady to v0.20.2 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 15c29941..5cd7c157 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.20.1 -- steady --version + npm exec --package=@stdy/cli@0.20.2 -- steady --version - npm exec --package=@stdy/cli@0.20.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.20.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index c8e2e9d5..b8143aa3 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.20.1 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.20.2 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" echo exit 1 From 51b8e108567aa503b73afd5926d2f8a869367119 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 16:53:12 +0000 Subject: [PATCH 382/399] docs(api): updates to API spec --- .stats.yml | 6 +- api.md | 25 +- src/writerai/_client.py | 2 +- src/writerai/resources/{tools => }/tools.py | 275 +----------------- src/writerai/resources/tools/__init__.py | 33 --- src/writerai/resources/tools/comprehend.py | 208 ------------- src/writerai/types/__init__.py | 6 - src/writerai/types/tool_ai_detect_params.py | 15 - src/writerai/types/tool_ai_detect_response.py | 13 - .../tool_context_aware_splitting_params.py | 19 -- .../tool_context_aware_splitting_response.py | 15 - src/writerai/types/tools/__init__.py | 3 - .../types/tools/comprehend_medical_params.py | 19 -- .../tools/comprehend_medical_response.py | 90 ------ tests/api_resources/test_tools.py | 153 +--------- tests/api_resources/tools/__init__.py | 1 - tests/api_resources/tools/test_comprehend.py | 102 ------- 17 files changed, 17 insertions(+), 968 deletions(-) rename src/writerai/resources/{tools => }/tools.py (69%) delete mode 100644 src/writerai/resources/tools/__init__.py delete mode 100644 src/writerai/resources/tools/comprehend.py delete mode 100644 src/writerai/types/tool_ai_detect_params.py delete mode 100644 src/writerai/types/tool_ai_detect_response.py delete mode 100644 src/writerai/types/tool_context_aware_splitting_params.py delete mode 100644 src/writerai/types/tool_context_aware_splitting_response.py delete mode 100644 src/writerai/types/tools/comprehend_medical_params.py delete mode 100644 src/writerai/types/tools/comprehend_medical_response.py delete mode 100644 tests/api_resources/tools/__init__.py delete mode 100644 tests/api_resources/tools/test_comprehend.py diff --git a/.stats.yml b/.stats.yml index a774d25b..4b198bb7 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 33 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-ea6ec4b34f6b7fdecc564f59b2e31482eee05830bf8dc1f389461b158de1548e.yml -openapi_spec_hash: ea89c1faed473908be2740efe6da255f +configured_endpoints: 30 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-ea6b4de3976794a02ea8fc01669d901cd7b159ba0d598cc9653e01c987a2f806.yml +openapi_spec_hash: 4d4a9ba232d19a6180e6d4a7d5566103 config_hash: 247c2ce23a36ef7446d356308329c87b diff --git a/api.md b/api.md index 4569c139..cd8c8e0e 100644 --- a/api.md +++ b/api.md @@ -162,32 +162,13 @@ Methods: Types: ```python -from writerai.types import ( - ToolAIDetectResponse, - ToolContextAwareSplittingResponse, - ToolParsePdfResponse, - ToolWebSearchResponse, -) -``` - -Methods: - -- client.tools.ai_detect(\*\*params) -> ToolAIDetectResponse -- client.tools.context_aware_splitting(\*\*params) -> ToolContextAwareSplittingResponse -- client.tools.parse_pdf(file_id, \*\*params) -> ToolParsePdfResponse -- client.tools.web_search(\*\*params) -> ToolWebSearchResponse - -## Comprehend - -Types: - -```python -from writerai.types.tools import ComprehendMedicalResponse +from writerai.types import ToolParsePdfResponse, ToolWebSearchResponse ``` Methods: -- client.tools.comprehend.medical(\*\*params) -> ComprehendMedicalResponse +- client.tools.parse_pdf(file_id, \*\*params) -> ToolParsePdfResponse +- client.tools.web_search(\*\*params) -> ToolWebSearchResponse # Translation diff --git a/src/writerai/_client.py b/src/writerai/_client.py index c4d23f42..29b42d83 100644 --- a/src/writerai/_client.py +++ b/src/writerai/_client.py @@ -34,11 +34,11 @@ from .resources import chat, files, tools, graphs, models, vision, completions, translation, applications from .resources.chat import ChatResource, AsyncChatResource from .resources.files import FilesResource, AsyncFilesResource + from .resources.tools import ToolsResource, AsyncToolsResource from .resources.graphs import GraphsResource, AsyncGraphsResource from .resources.models import ModelsResource, AsyncModelsResource from .resources.vision import VisionResource, AsyncVisionResource from .resources.completions import CompletionsResource, AsyncCompletionsResource - from .resources.tools.tools import ToolsResource, AsyncToolsResource from .resources.translation import TranslationResource, AsyncTranslationResource from .resources.applications.applications import ApplicationsResource, AsyncApplicationsResource diff --git a/src/writerai/resources/tools/tools.py b/src/writerai/resources/tools.py similarity index 69% rename from src/writerai/resources/tools/tools.py rename to src/writerai/resources/tools.py index c74050d6..8fd00630 100644 --- a/src/writerai/resources/tools/tools.py +++ b/src/writerai/resources/tools.py @@ -8,44 +8,25 @@ import httpx -from ...types import ( - tool_ai_detect_params, - tool_parse_pdf_params, - tool_web_search_params, - tool_context_aware_splitting_params, -) -from ..._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given -from ..._utils import path_template, maybe_transform, async_maybe_transform -from ..._compat import cached_property -from .comprehend import ( - ComprehendResource, - AsyncComprehendResource, - ComprehendResourceWithRawResponse, - AsyncComprehendResourceWithRawResponse, - ComprehendResourceWithStreamingResponse, - AsyncComprehendResourceWithStreamingResponse, -) -from ..._resource import SyncAPIResource, AsyncAPIResource -from ..._response import ( +from ..types import tool_parse_pdf_params, tool_web_search_params +from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given +from .._utils import path_template, maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( to_raw_response_wrapper, to_streamed_response_wrapper, async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from ..._base_client import make_request_options -from ...types.tool_ai_detect_response import ToolAIDetectResponse -from ...types.tool_parse_pdf_response import ToolParsePdfResponse -from ...types.tool_web_search_response import ToolWebSearchResponse -from ...types.tool_context_aware_splitting_response import ToolContextAwareSplittingResponse +from .._base_client import make_request_options +from ..types.tool_parse_pdf_response import ToolParsePdfResponse +from ..types.tool_web_search_response import ToolWebSearchResponse __all__ = ["ToolsResource", "AsyncToolsResource"] class ToolsResource(SyncAPIResource): - @cached_property - def comprehend(self) -> ComprehendResource: - return ComprehendResource(self._client) - @cached_property def with_raw_response(self) -> ToolsResourceWithRawResponse: """ @@ -65,95 +46,6 @@ def with_streaming_response(self) -> ToolsResourceWithStreamingResponse: """ return ToolsResourceWithStreamingResponse(self) - @typing_extensions.deprecated( - "Will be removed in a future release. Please migrate to alternative solutions. See documentation at dev.writer.com for more information." - ) - def ai_detect( - self, - *, - input: str, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ToolAIDetectResponse: - """Detects if content is AI- or human-generated, with a confidence score. - - Content - must have at least 350 characters - - Args: - input: The content to determine if it is AI- or human-generated. Content must have at - least 350 characters. - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return self._post( - "/v1/tools/ai-detect", - body=maybe_transform({"input": input}, tool_ai_detect_params.ToolAIDetectParams), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ToolAIDetectResponse, - ) - - @typing_extensions.deprecated( - "Will be removed in a future release. Please migrate to alternative solutions. See documentation at dev.writer.com for more information." - ) - def context_aware_splitting( - self, - *, - strategy: Literal["llm_split", "fast_split", "hybrid_split"], - text: str, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ToolContextAwareSplittingResponse: - """ - Splits a long block of text (maximum 4000 words) into smaller chunks while - preserving the semantic meaning of the text and context between the chunks. - - Args: - strategy: The strategy to use for splitting the text into chunks. `llm_split` uses the - language model to split the text, `fast_split` uses a fast heuristic-based - approach, and `hybrid_split` combines both strategies. - - text: The text to split into chunks. - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return self._post( - "/v1/tools/context-aware-splitting", - body=maybe_transform( - { - "strategy": strategy, - "text": text, - }, - tool_context_aware_splitting_params.ToolContextAwareSplittingParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ToolContextAwareSplittingResponse, - ) - @typing_extensions.deprecated( "Will be removed in a future release. A replacement PDF parsing tool for chat completions is planned; see documentation at dev.writer.com for more information." ) @@ -472,10 +364,6 @@ def web_search( class AsyncToolsResource(AsyncAPIResource): - @cached_property - def comprehend(self) -> AsyncComprehendResource: - return AsyncComprehendResource(self._client) - @cached_property def with_raw_response(self) -> AsyncToolsResourceWithRawResponse: """ @@ -495,95 +383,6 @@ def with_streaming_response(self) -> AsyncToolsResourceWithStreamingResponse: """ return AsyncToolsResourceWithStreamingResponse(self) - @typing_extensions.deprecated( - "Will be removed in a future release. Please migrate to alternative solutions. See documentation at dev.writer.com for more information." - ) - async def ai_detect( - self, - *, - input: str, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ToolAIDetectResponse: - """Detects if content is AI- or human-generated, with a confidence score. - - Content - must have at least 350 characters - - Args: - input: The content to determine if it is AI- or human-generated. Content must have at - least 350 characters. - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return await self._post( - "/v1/tools/ai-detect", - body=await async_maybe_transform({"input": input}, tool_ai_detect_params.ToolAIDetectParams), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ToolAIDetectResponse, - ) - - @typing_extensions.deprecated( - "Will be removed in a future release. Please migrate to alternative solutions. See documentation at dev.writer.com for more information." - ) - async def context_aware_splitting( - self, - *, - strategy: Literal["llm_split", "fast_split", "hybrid_split"], - text: str, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ToolContextAwareSplittingResponse: - """ - Splits a long block of text (maximum 4000 words) into smaller chunks while - preserving the semantic meaning of the text and context between the chunks. - - Args: - strategy: The strategy to use for splitting the text into chunks. `llm_split` uses the - language model to split the text, `fast_split` uses a fast heuristic-based - approach, and `hybrid_split` combines both strategies. - - text: The text to split into chunks. - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return await self._post( - "/v1/tools/context-aware-splitting", - body=await async_maybe_transform( - { - "strategy": strategy, - "text": text, - }, - tool_context_aware_splitting_params.ToolContextAwareSplittingParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ToolContextAwareSplittingResponse, - ) - @typing_extensions.deprecated( "Will be removed in a future release. A replacement PDF parsing tool for chat completions is planned; see documentation at dev.writer.com for more information." ) @@ -905,16 +704,6 @@ class ToolsResourceWithRawResponse: def __init__(self, tools: ToolsResource) -> None: self._tools = tools - self.ai_detect = ( # pyright: ignore[reportDeprecated] - to_raw_response_wrapper( - tools.ai_detect, # pyright: ignore[reportDeprecated], - ) - ) - self.context_aware_splitting = ( # pyright: ignore[reportDeprecated] - to_raw_response_wrapper( - tools.context_aware_splitting, # pyright: ignore[reportDeprecated], - ) - ) self.parse_pdf = ( # pyright: ignore[reportDeprecated] to_raw_response_wrapper( tools.parse_pdf, # pyright: ignore[reportDeprecated], @@ -926,25 +715,11 @@ def __init__(self, tools: ToolsResource) -> None: ) ) - @cached_property - def comprehend(self) -> ComprehendResourceWithRawResponse: - return ComprehendResourceWithRawResponse(self._tools.comprehend) - class AsyncToolsResourceWithRawResponse: def __init__(self, tools: AsyncToolsResource) -> None: self._tools = tools - self.ai_detect = ( # pyright: ignore[reportDeprecated] - async_to_raw_response_wrapper( - tools.ai_detect, # pyright: ignore[reportDeprecated], - ) - ) - self.context_aware_splitting = ( # pyright: ignore[reportDeprecated] - async_to_raw_response_wrapper( - tools.context_aware_splitting, # pyright: ignore[reportDeprecated], - ) - ) self.parse_pdf = ( # pyright: ignore[reportDeprecated] async_to_raw_response_wrapper( tools.parse_pdf, # pyright: ignore[reportDeprecated], @@ -956,25 +731,11 @@ def __init__(self, tools: AsyncToolsResource) -> None: ) ) - @cached_property - def comprehend(self) -> AsyncComprehendResourceWithRawResponse: - return AsyncComprehendResourceWithRawResponse(self._tools.comprehend) - class ToolsResourceWithStreamingResponse: def __init__(self, tools: ToolsResource) -> None: self._tools = tools - self.ai_detect = ( # pyright: ignore[reportDeprecated] - to_streamed_response_wrapper( - tools.ai_detect, # pyright: ignore[reportDeprecated], - ) - ) - self.context_aware_splitting = ( # pyright: ignore[reportDeprecated] - to_streamed_response_wrapper( - tools.context_aware_splitting, # pyright: ignore[reportDeprecated], - ) - ) self.parse_pdf = ( # pyright: ignore[reportDeprecated] to_streamed_response_wrapper( tools.parse_pdf, # pyright: ignore[reportDeprecated], @@ -986,25 +747,11 @@ def __init__(self, tools: ToolsResource) -> None: ) ) - @cached_property - def comprehend(self) -> ComprehendResourceWithStreamingResponse: - return ComprehendResourceWithStreamingResponse(self._tools.comprehend) - class AsyncToolsResourceWithStreamingResponse: def __init__(self, tools: AsyncToolsResource) -> None: self._tools = tools - self.ai_detect = ( # pyright: ignore[reportDeprecated] - async_to_streamed_response_wrapper( - tools.ai_detect, # pyright: ignore[reportDeprecated], - ) - ) - self.context_aware_splitting = ( # pyright: ignore[reportDeprecated] - async_to_streamed_response_wrapper( - tools.context_aware_splitting, # pyright: ignore[reportDeprecated], - ) - ) self.parse_pdf = ( # pyright: ignore[reportDeprecated] async_to_streamed_response_wrapper( tools.parse_pdf, # pyright: ignore[reportDeprecated], @@ -1015,7 +762,3 @@ def __init__(self, tools: AsyncToolsResource) -> None: tools.web_search, # pyright: ignore[reportDeprecated], ) ) - - @cached_property - def comprehend(self) -> AsyncComprehendResourceWithStreamingResponse: - return AsyncComprehendResourceWithStreamingResponse(self._tools.comprehend) diff --git a/src/writerai/resources/tools/__init__.py b/src/writerai/resources/tools/__init__.py deleted file mode 100644 index 8f4ceef3..00000000 --- a/src/writerai/resources/tools/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from .tools import ( - ToolsResource, - AsyncToolsResource, - ToolsResourceWithRawResponse, - AsyncToolsResourceWithRawResponse, - ToolsResourceWithStreamingResponse, - AsyncToolsResourceWithStreamingResponse, -) -from .comprehend import ( - ComprehendResource, - AsyncComprehendResource, - ComprehendResourceWithRawResponse, - AsyncComprehendResourceWithRawResponse, - ComprehendResourceWithStreamingResponse, - AsyncComprehendResourceWithStreamingResponse, -) - -__all__ = [ - "ComprehendResource", - "AsyncComprehendResource", - "ComprehendResourceWithRawResponse", - "AsyncComprehendResourceWithRawResponse", - "ComprehendResourceWithStreamingResponse", - "AsyncComprehendResourceWithStreamingResponse", - "ToolsResource", - "AsyncToolsResource", - "ToolsResourceWithRawResponse", - "AsyncToolsResourceWithRawResponse", - "ToolsResourceWithStreamingResponse", - "AsyncToolsResourceWithStreamingResponse", -] diff --git a/src/writerai/resources/tools/comprehend.py b/src/writerai/resources/tools/comprehend.py deleted file mode 100644 index 2cff5f4f..00000000 --- a/src/writerai/resources/tools/comprehend.py +++ /dev/null @@ -1,208 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import typing_extensions -from typing_extensions import Literal - -import httpx - -from ..._types import Body, Query, Headers, NotGiven, not_given -from ..._utils import maybe_transform, async_maybe_transform -from ..._compat import cached_property -from ..._resource import SyncAPIResource, AsyncAPIResource -from ..._response import ( - to_raw_response_wrapper, - to_streamed_response_wrapper, - async_to_raw_response_wrapper, - async_to_streamed_response_wrapper, -) -from ...types.tools import comprehend_medical_params -from ..._base_client import make_request_options -from ...types.tools.comprehend_medical_response import ComprehendMedicalResponse - -__all__ = ["ComprehendResource", "AsyncComprehendResource"] - - -class ComprehendResource(SyncAPIResource): - @cached_property - def with_raw_response(self) -> ComprehendResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers - """ - return ComprehendResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> ComprehendResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/writer/writer-python#with_streaming_response - """ - return ComprehendResourceWithStreamingResponse(self) - - @typing_extensions.deprecated( - "Will be removed in a future release. Migrate to `chat.chat` with the LLM tool using the `palmyra-med` model for medical analysis. See documentation at dev.writer.com for more information." - ) - def medical( - self, - *, - content: str, - response_type: Literal["Entities", "RxNorm", "ICD-10-CM", "SNOMED CT"], - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ComprehendMedicalResponse: - """ - Analyze unstructured medical text to extract entities labeled with standardized - medical codes and confidence scores. - - Args: - content: The text to analyze. - - response_type: The structure of the response to return. `Entities` returns medical entities, - `RxNorm` returns medication information, `ICD-10-CM` returns diagnosis codes, - and `SNOMED CT` returns medical concepts. - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return self._post( - "/v1/tools/comprehend/medical", - body=maybe_transform( - { - "content": content, - "response_type": response_type, - }, - comprehend_medical_params.ComprehendMedicalParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ComprehendMedicalResponse, - ) - - -class AsyncComprehendResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncComprehendResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/writer/writer-python#accessing-raw-response-data-eg-headers - """ - return AsyncComprehendResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncComprehendResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/writer/writer-python#with_streaming_response - """ - return AsyncComprehendResourceWithStreamingResponse(self) - - @typing_extensions.deprecated( - "Will be removed in a future release. Migrate to `chat.chat` with the LLM tool using the `palmyra-med` model for medical analysis. See documentation at dev.writer.com for more information." - ) - async def medical( - self, - *, - content: str, - response_type: Literal["Entities", "RxNorm", "ICD-10-CM", "SNOMED CT"], - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ComprehendMedicalResponse: - """ - Analyze unstructured medical text to extract entities labeled with standardized - medical codes and confidence scores. - - Args: - content: The text to analyze. - - response_type: The structure of the response to return. `Entities` returns medical entities, - `RxNorm` returns medication information, `ICD-10-CM` returns diagnosis codes, - and `SNOMED CT` returns medical concepts. - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return await self._post( - "/v1/tools/comprehend/medical", - body=await async_maybe_transform( - { - "content": content, - "response_type": response_type, - }, - comprehend_medical_params.ComprehendMedicalParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ComprehendMedicalResponse, - ) - - -class ComprehendResourceWithRawResponse: - def __init__(self, comprehend: ComprehendResource) -> None: - self._comprehend = comprehend - - self.medical = ( # pyright: ignore[reportDeprecated] - to_raw_response_wrapper( - comprehend.medical, # pyright: ignore[reportDeprecated], - ) - ) - - -class AsyncComprehendResourceWithRawResponse: - def __init__(self, comprehend: AsyncComprehendResource) -> None: - self._comprehend = comprehend - - self.medical = ( # pyright: ignore[reportDeprecated] - async_to_raw_response_wrapper( - comprehend.medical, # pyright: ignore[reportDeprecated], - ) - ) - - -class ComprehendResourceWithStreamingResponse: - def __init__(self, comprehend: ComprehendResource) -> None: - self._comprehend = comprehend - - self.medical = ( # pyright: ignore[reportDeprecated] - to_streamed_response_wrapper( - comprehend.medical, # pyright: ignore[reportDeprecated], - ) - ) - - -class AsyncComprehendResourceWithStreamingResponse: - def __init__(self, comprehend: AsyncComprehendResource) -> None: - self._comprehend = comprehend - - self.medical = ( # pyright: ignore[reportDeprecated] - async_to_streamed_response_wrapper( - comprehend.medical, # pyright: ignore[reportDeprecated], - ) - ) diff --git a/src/writerai/types/__init__.py b/src/writerai/types/__init__.py index 3ded6249..195d442a 100644 --- a/src/writerai/types/__init__.py +++ b/src/writerai/types/__init__.py @@ -41,7 +41,6 @@ from .graph_delete_response import GraphDeleteResponse as GraphDeleteResponse from .graph_question_params import GraphQuestionParams as GraphQuestionParams from .graph_update_response import GraphUpdateResponse as GraphUpdateResponse -from .tool_ai_detect_params import ToolAIDetectParams as ToolAIDetectParams from .tool_parse_pdf_params import ToolParsePdfParams as ToolParsePdfParams from .vision_analyze_params import VisionAnalyzeParams as VisionAnalyzeParams from .chat_completion_choice import ChatCompletionChoice as ChatCompletionChoice @@ -49,7 +48,6 @@ from .application_list_params import ApplicationListParams as ApplicationListParams from .chat_completion_message import ChatCompletionMessage as ChatCompletionMessage from .question_response_chunk import QuestionResponseChunk as QuestionResponseChunk -from .tool_ai_detect_response import ToolAIDetectResponse as ToolAIDetectResponse from .tool_parse_pdf_response import ToolParsePdfResponse as ToolParsePdfResponse from .completion_create_params import CompletionCreateParams as CompletionCreateParams from .tool_web_search_response import ToolWebSearchResponse as ToolWebSearchResponse @@ -59,11 +57,7 @@ from .graph_add_file_to_graph_params import GraphAddFileToGraphParams as GraphAddFileToGraphParams from .application_generate_content_chunk import ApplicationGenerateContentChunk as ApplicationGenerateContentChunk from .application_generate_content_params import ApplicationGenerateContentParams as ApplicationGenerateContentParams -from .tool_context_aware_splitting_params import ToolContextAwareSplittingParams as ToolContextAwareSplittingParams from .application_generate_content_response import ( ApplicationGenerateContentResponse as ApplicationGenerateContentResponse, ) from .graph_remove_file_from_graph_response import GraphRemoveFileFromGraphResponse as GraphRemoveFileFromGraphResponse -from .tool_context_aware_splitting_response import ( - ToolContextAwareSplittingResponse as ToolContextAwareSplittingResponse, -) diff --git a/src/writerai/types/tool_ai_detect_params.py b/src/writerai/types/tool_ai_detect_params.py deleted file mode 100644 index e162d4c3..00000000 --- a/src/writerai/types/tool_ai_detect_params.py +++ /dev/null @@ -1,15 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Required, TypedDict - -__all__ = ["ToolAIDetectParams"] - - -class ToolAIDetectParams(TypedDict, total=False): - input: Required[str] - """The content to determine if it is AI- or human-generated. - - Content must have at least 350 characters. - """ diff --git a/src/writerai/types/tool_ai_detect_response.py b/src/writerai/types/tool_ai_detect_response.py deleted file mode 100644 index 48052a29..00000000 --- a/src/writerai/types/tool_ai_detect_response.py +++ /dev/null @@ -1,13 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing_extensions import Literal - -from .._models import BaseModel - -__all__ = ["ToolAIDetectResponse"] - - -class ToolAIDetectResponse(BaseModel): - label: Literal["fake", "real"] - - score: float diff --git a/src/writerai/types/tool_context_aware_splitting_params.py b/src/writerai/types/tool_context_aware_splitting_params.py deleted file mode 100644 index eb94d79d..00000000 --- a/src/writerai/types/tool_context_aware_splitting_params.py +++ /dev/null @@ -1,19 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Literal, Required, TypedDict - -__all__ = ["ToolContextAwareSplittingParams"] - - -class ToolContextAwareSplittingParams(TypedDict, total=False): - strategy: Required[Literal["llm_split", "fast_split", "hybrid_split"]] - """The strategy to use for splitting the text into chunks. - - `llm_split` uses the language model to split the text, `fast_split` uses a fast - heuristic-based approach, and `hybrid_split` combines both strategies. - """ - - text: Required[str] - """The text to split into chunks.""" diff --git a/src/writerai/types/tool_context_aware_splitting_response.py b/src/writerai/types/tool_context_aware_splitting_response.py deleted file mode 100644 index 74f3a773..00000000 --- a/src/writerai/types/tool_context_aware_splitting_response.py +++ /dev/null @@ -1,15 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List - -from .._models import BaseModel - -__all__ = ["ToolContextAwareSplittingResponse"] - - -class ToolContextAwareSplittingResponse(BaseModel): - chunks: List[str] - """ - An array of text chunks generated by splitting the input text based on the - specified strategy. - """ diff --git a/src/writerai/types/tools/__init__.py b/src/writerai/types/tools/__init__.py index 23e03174..f8ee8b14 100644 --- a/src/writerai/types/tools/__init__.py +++ b/src/writerai/types/tools/__init__.py @@ -1,6 +1,3 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from __future__ import annotations - -from .comprehend_medical_params import ComprehendMedicalParams as ComprehendMedicalParams -from .comprehend_medical_response import ComprehendMedicalResponse as ComprehendMedicalResponse diff --git a/src/writerai/types/tools/comprehend_medical_params.py b/src/writerai/types/tools/comprehend_medical_params.py deleted file mode 100644 index 6377654f..00000000 --- a/src/writerai/types/tools/comprehend_medical_params.py +++ /dev/null @@ -1,19 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Literal, Required, TypedDict - -__all__ = ["ComprehendMedicalParams"] - - -class ComprehendMedicalParams(TypedDict, total=False): - content: Required[str] - """The text to analyze.""" - - response_type: Required[Literal["Entities", "RxNorm", "ICD-10-CM", "SNOMED CT"]] - """The structure of the response to return. - - `Entities` returns medical entities, `RxNorm` returns medication information, - `ICD-10-CM` returns diagnosis codes, and `SNOMED CT` returns medical concepts. - """ diff --git a/src/writerai/types/tools/comprehend_medical_response.py b/src/writerai/types/tools/comprehend_medical_response.py deleted file mode 100644 index 9489f389..00000000 --- a/src/writerai/types/tools/comprehend_medical_response.py +++ /dev/null @@ -1,90 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List, Optional - -from ..._models import BaseModel - -__all__ = [ - "ComprehendMedicalResponse", - "Entity", - "EntityAttribute", - "EntityAttributeConcept", - "EntityAttributeTrait", - "EntityConcept", - "EntityTrait", -] - - -class EntityAttributeConcept(BaseModel): - code: str - - description: str - - score: float - - -class EntityAttributeTrait(BaseModel): - name: str - - score: float - - -class EntityAttribute(BaseModel): - begin_offset: int - - concepts: List[EntityAttributeConcept] - - end_offset: int - - relationship_score: float - - score: float - - text: str - - traits: List[EntityAttributeTrait] - - type: str - - category: Optional[str] = None - - relationship_type: Optional[str] = None - - -class EntityConcept(BaseModel): - code: str - - description: str - - score: float - - -class EntityTrait(BaseModel): - name: str - - score: float - - -class Entity(BaseModel): - attributes: List[EntityAttribute] - - begin_offset: int - - category: str - - concepts: List[EntityConcept] - - end_offset: int - - score: float - - text: str - - traits: List[EntityTrait] - - type: str - - -class ComprehendMedicalResponse(BaseModel): - entities: List[Entity] - """An array of medical entities extracted from the input text.""" diff --git a/tests/api_resources/test_tools.py b/tests/api_resources/test_tools.py index 971657ee..8e0bd7a7 100644 --- a/tests/api_resources/test_tools.py +++ b/tests/api_resources/test_tools.py @@ -9,12 +9,7 @@ from writerai import Writer, AsyncWriter from tests.utils import assert_matches_type -from writerai.types import ( - ToolAIDetectResponse, - ToolParsePdfResponse, - ToolWebSearchResponse, - ToolContextAwareSplittingResponse, -) +from writerai.types import ToolParsePdfResponse, ToolWebSearchResponse # pyright: reportDeprecated=false @@ -24,79 +19,6 @@ class TestTools: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @parametrize - def test_method_ai_detect(self, client: Writer) -> None: - with pytest.warns(DeprecationWarning): - tool = client.tools.ai_detect( - input="AI and ML continue to be at the forefront of technological advancements. In 2025, we can expect more sophisticated AI systems that can handle complex tasks with greater efficiency. AI will play a crucial role in various sectors, including healthcare, finance, and manufacturing. For instance, AI-powered diagnostic tools will become more accurate, helping doctors detect diseases at an early stage. In finance, AI algorithms will enhance fraud detection and risk management.", - ) - - assert_matches_type(ToolAIDetectResponse, tool, path=["response"]) - - @parametrize - def test_raw_response_ai_detect(self, client: Writer) -> None: - with pytest.warns(DeprecationWarning): - response = client.tools.with_raw_response.ai_detect( - input="AI and ML continue to be at the forefront of technological advancements. In 2025, we can expect more sophisticated AI systems that can handle complex tasks with greater efficiency. AI will play a crucial role in various sectors, including healthcare, finance, and manufacturing. For instance, AI-powered diagnostic tools will become more accurate, helping doctors detect diseases at an early stage. In finance, AI algorithms will enhance fraud detection and risk management.", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - tool = response.parse() - assert_matches_type(ToolAIDetectResponse, tool, path=["response"]) - - @parametrize - def test_streaming_response_ai_detect(self, client: Writer) -> None: - with pytest.warns(DeprecationWarning): - with client.tools.with_streaming_response.ai_detect( - input="AI and ML continue to be at the forefront of technological advancements. In 2025, we can expect more sophisticated AI systems that can handle complex tasks with greater efficiency. AI will play a crucial role in various sectors, including healthcare, finance, and manufacturing. For instance, AI-powered diagnostic tools will become more accurate, helping doctors detect diseases at an early stage. In finance, AI algorithms will enhance fraud detection and risk management.", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - tool = response.parse() - assert_matches_type(ToolAIDetectResponse, tool, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @parametrize - def test_method_context_aware_splitting(self, client: Writer) -> None: - with pytest.warns(DeprecationWarning): - tool = client.tools.context_aware_splitting( - strategy="llm_split", - text="text", - ) - - assert_matches_type(ToolContextAwareSplittingResponse, tool, path=["response"]) - - @parametrize - def test_raw_response_context_aware_splitting(self, client: Writer) -> None: - with pytest.warns(DeprecationWarning): - response = client.tools.with_raw_response.context_aware_splitting( - strategy="llm_split", - text="text", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - tool = response.parse() - assert_matches_type(ToolContextAwareSplittingResponse, tool, path=["response"]) - - @parametrize - def test_streaming_response_context_aware_splitting(self, client: Writer) -> None: - with pytest.warns(DeprecationWarning): - with client.tools.with_streaming_response.context_aware_splitting( - strategy="llm_split", - text="text", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - tool = response.parse() - assert_matches_type(ToolContextAwareSplittingResponse, tool, path=["response"]) - - assert cast(Any, response.is_closed) is True - @parametrize def test_method_parse_pdf(self, client: Writer) -> None: with pytest.warns(DeprecationWarning): @@ -200,79 +122,6 @@ class TestAsyncTools: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @parametrize - async def test_method_ai_detect(self, async_client: AsyncWriter) -> None: - with pytest.warns(DeprecationWarning): - tool = await async_client.tools.ai_detect( - input="AI and ML continue to be at the forefront of technological advancements. In 2025, we can expect more sophisticated AI systems that can handle complex tasks with greater efficiency. AI will play a crucial role in various sectors, including healthcare, finance, and manufacturing. For instance, AI-powered diagnostic tools will become more accurate, helping doctors detect diseases at an early stage. In finance, AI algorithms will enhance fraud detection and risk management.", - ) - - assert_matches_type(ToolAIDetectResponse, tool, path=["response"]) - - @parametrize - async def test_raw_response_ai_detect(self, async_client: AsyncWriter) -> None: - with pytest.warns(DeprecationWarning): - response = await async_client.tools.with_raw_response.ai_detect( - input="AI and ML continue to be at the forefront of technological advancements. In 2025, we can expect more sophisticated AI systems that can handle complex tasks with greater efficiency. AI will play a crucial role in various sectors, including healthcare, finance, and manufacturing. For instance, AI-powered diagnostic tools will become more accurate, helping doctors detect diseases at an early stage. In finance, AI algorithms will enhance fraud detection and risk management.", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - tool = await response.parse() - assert_matches_type(ToolAIDetectResponse, tool, path=["response"]) - - @parametrize - async def test_streaming_response_ai_detect(self, async_client: AsyncWriter) -> None: - with pytest.warns(DeprecationWarning): - async with async_client.tools.with_streaming_response.ai_detect( - input="AI and ML continue to be at the forefront of technological advancements. In 2025, we can expect more sophisticated AI systems that can handle complex tasks with greater efficiency. AI will play a crucial role in various sectors, including healthcare, finance, and manufacturing. For instance, AI-powered diagnostic tools will become more accurate, helping doctors detect diseases at an early stage. In finance, AI algorithms will enhance fraud detection and risk management.", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - tool = await response.parse() - assert_matches_type(ToolAIDetectResponse, tool, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @parametrize - async def test_method_context_aware_splitting(self, async_client: AsyncWriter) -> None: - with pytest.warns(DeprecationWarning): - tool = await async_client.tools.context_aware_splitting( - strategy="llm_split", - text="text", - ) - - assert_matches_type(ToolContextAwareSplittingResponse, tool, path=["response"]) - - @parametrize - async def test_raw_response_context_aware_splitting(self, async_client: AsyncWriter) -> None: - with pytest.warns(DeprecationWarning): - response = await async_client.tools.with_raw_response.context_aware_splitting( - strategy="llm_split", - text="text", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - tool = await response.parse() - assert_matches_type(ToolContextAwareSplittingResponse, tool, path=["response"]) - - @parametrize - async def test_streaming_response_context_aware_splitting(self, async_client: AsyncWriter) -> None: - with pytest.warns(DeprecationWarning): - async with async_client.tools.with_streaming_response.context_aware_splitting( - strategy="llm_split", - text="text", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - tool = await response.parse() - assert_matches_type(ToolContextAwareSplittingResponse, tool, path=["response"]) - - assert cast(Any, response.is_closed) is True - @parametrize async def test_method_parse_pdf(self, async_client: AsyncWriter) -> None: with pytest.warns(DeprecationWarning): diff --git a/tests/api_resources/tools/__init__.py b/tests/api_resources/tools/__init__.py deleted file mode 100644 index fd8019a9..00000000 --- a/tests/api_resources/tools/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/tools/test_comprehend.py b/tests/api_resources/tools/test_comprehend.py deleted file mode 100644 index 59e9dd9f..00000000 --- a/tests/api_resources/tools/test_comprehend.py +++ /dev/null @@ -1,102 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import os -from typing import Any, cast - -import pytest - -from writerai import Writer, AsyncWriter -from tests.utils import assert_matches_type -from writerai.types.tools import ComprehendMedicalResponse - -# pyright: reportDeprecated=false - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestComprehend: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @parametrize - def test_method_medical(self, client: Writer) -> None: - with pytest.warns(DeprecationWarning): - comprehend = client.tools.comprehend.medical( - content="content", - response_type="Entities", - ) - - assert_matches_type(ComprehendMedicalResponse, comprehend, path=["response"]) - - @parametrize - def test_raw_response_medical(self, client: Writer) -> None: - with pytest.warns(DeprecationWarning): - response = client.tools.comprehend.with_raw_response.medical( - content="content", - response_type="Entities", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - comprehend = response.parse() - assert_matches_type(ComprehendMedicalResponse, comprehend, path=["response"]) - - @parametrize - def test_streaming_response_medical(self, client: Writer) -> None: - with pytest.warns(DeprecationWarning): - with client.tools.comprehend.with_streaming_response.medical( - content="content", - response_type="Entities", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - comprehend = response.parse() - assert_matches_type(ComprehendMedicalResponse, comprehend, path=["response"]) - - assert cast(Any, response.is_closed) is True - - -class TestAsyncComprehend: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) - - @parametrize - async def test_method_medical(self, async_client: AsyncWriter) -> None: - with pytest.warns(DeprecationWarning): - comprehend = await async_client.tools.comprehend.medical( - content="content", - response_type="Entities", - ) - - assert_matches_type(ComprehendMedicalResponse, comprehend, path=["response"]) - - @parametrize - async def test_raw_response_medical(self, async_client: AsyncWriter) -> None: - with pytest.warns(DeprecationWarning): - response = await async_client.tools.comprehend.with_raw_response.medical( - content="content", - response_type="Entities", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - comprehend = await response.parse() - assert_matches_type(ComprehendMedicalResponse, comprehend, path=["response"]) - - @parametrize - async def test_streaming_response_medical(self, async_client: AsyncWriter) -> None: - with pytest.warns(DeprecationWarning): - async with async_client.tools.comprehend.with_streaming_response.medical( - content="content", - response_type="Entities", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - comprehend = await response.parse() - assert_matches_type(ComprehendMedicalResponse, comprehend, path=["response"]) - - assert cast(Any, response.is_closed) is True From a66361bf9c7c425583c280ec272d414536fa144c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 16:13:58 +0000 Subject: [PATCH 383/399] fix(client): preserve hardcoded query params when merging with user params --- src/writerai/_base_client.py | 4 +++ tests/test_client.py | 48 ++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index 62fb9f9b..22c0ce8f 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -540,6 +540,10 @@ def _build_request( files = cast(HttpxRequestFiles, ForceMultipartDict()) prepared_url = self._prepare_url(options.url) + # preserve hard-coded query params from the url + if params and prepared_url.query: + params = {**dict(prepared_url.params.items()), **params} + prepared_url = prepared_url.copy_with(raw_path=prepared_url.raw_path.split(b"?", 1)[0]) if "_" in prepared_url.host: # work around https://github.com/encode/httpx/discussions/2880 kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")} diff --git a/tests/test_client.py b/tests/test_client.py index b13036e5..2b13f897 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -428,6 +428,30 @@ def test_default_query_option(self) -> None: client.close() + def test_hardcoded_query_params_in_url(self, client: Writer) -> None: + request = client._build_request(FinalRequestOptions(method="get", url="/foo?beta=true")) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/foo?beta=true", + params={"limit": "10", "page": "abc"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true", "limit": "10", "page": "abc"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/files/a%2Fb?beta=true", + params={"limit": "10"}, + ) + ) + assert request.url.raw_path == b"/files/a%2Fb?beta=true&limit=10" + def test_request_extra_json(self, client: Writer) -> None: request = client._build_request( FinalRequestOptions( @@ -1334,6 +1358,30 @@ async def test_default_query_option(self) -> None: await client.close() + async def test_hardcoded_query_params_in_url(self, async_client: AsyncWriter) -> None: + request = async_client._build_request(FinalRequestOptions(method="get", url="/foo?beta=true")) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true"} + + request = async_client._build_request( + FinalRequestOptions( + method="get", + url="/foo?beta=true", + params={"limit": "10", "page": "abc"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true", "limit": "10", "page": "abc"} + + request = async_client._build_request( + FinalRequestOptions( + method="get", + url="/files/a%2Fb?beta=true", + params={"limit": "10"}, + ) + ) + assert request.url.raw_path == b"/files/a%2Fb?beta=true&limit=10" + def test_request_extra_json(self, client: Writer) -> None: request = client._build_request( FinalRequestOptions( From 9baf22f39f6000cb60a4647623b3f11ac9a32c55 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 9 Apr 2026 19:46:25 +0000 Subject: [PATCH 384/399] feat(api): Deprecate AI Detection, Medical Comprehend, and Context-Aware Text Splitting Remove AI Detection, Medical Comprehend, text to graph, and Context-Aware Text Splitting from SDKs. --- .stats.yml | 2 +- src/writerai/types/tools/__init__.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) delete mode 100644 src/writerai/types/tools/__init__.py diff --git a/.stats.yml b/.stats.yml index 4b198bb7..18b117f4 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-ea6b4de3976794a02ea8fc01669d901cd7b159ba0d598cc9653e01c987a2f806.yml openapi_spec_hash: 4d4a9ba232d19a6180e6d4a7d5566103 -config_hash: 247c2ce23a36ef7446d356308329c87b +config_hash: 8701b1a467238f1afdeceeb7feb1adc6 diff --git a/src/writerai/types/tools/__init__.py b/src/writerai/types/tools/__init__.py deleted file mode 100644 index f8ee8b14..00000000 --- a/src/writerai/types/tools/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations From d88b2bfc94c7a90cf765f52f77b49dd7a0b6872f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 15:01:05 +0000 Subject: [PATCH 385/399] fix: ensure file data are only sent as 1 parameter --- src/writerai/_utils/_utils.py | 5 +++-- tests/test_extract_files.py | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/writerai/_utils/_utils.py b/src/writerai/_utils/_utils.py index eec7f4a1..63b8cd60 100644 --- a/src/writerai/_utils/_utils.py +++ b/src/writerai/_utils/_utils.py @@ -86,8 +86,9 @@ def _extract_items( index += 1 if is_dict(obj): try: - # We are at the last entry in the path so we must remove the field - if (len(path)) == index: + # Remove the field if there are no more dict keys in the path, + # only "" traversal markers or end. + if all(p == "" for p in path[index:]): item = obj.pop(key) else: item = obj[key] diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py index 9d9a4b19..436354bf 100644 --- a/tests/test_extract_files.py +++ b/tests/test_extract_files.py @@ -35,6 +35,15 @@ def test_multiple_files() -> None: assert query == {"documents": [{}, {}]} +def test_top_level_file_array() -> None: + query = {"files": [b"file one", b"file two"], "title": "hello"} + assert extract_files(query, paths=[["files", ""]]) == [ + ("files[]", b"file one"), + ("files[]", b"file two"), + ] + assert query == {"title": "hello"} + + @pytest.mark.parametrize( "query,paths,expected", [ From a228fe5875c8cde09a847aa47a11a1f1fda24019 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 20:15:03 +0000 Subject: [PATCH 386/399] chore(internal): version bump --- .release-please-manifest.json | 2 +- README.md | 4 ++-- pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index b44b2870..0ec9934d 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.4.0" + ".": "3.0.0-rc1" } \ No newline at end of file diff --git a/README.md b/README.md index 8be84e69..b3542e5f 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ The REST API documentation can be found on [dev.writer.com](https://dev.writer.c ```sh # install from PyPI -pip install writer-sdk +pip install '--pre writer-sdk' ``` ## Usage @@ -98,7 +98,7 @@ You can enable this by installing `aiohttp`: ```sh # install from PyPI -pip install writer-sdk[aiohttp] +pip install '--pre writer-sdk[aiohttp]' ``` Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: diff --git a/pyproject.toml b/pyproject.toml index 5bcf8651..72168545 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "2.4.0" +version = "3.0.0-rc1" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index 227300e7..26d04ff0 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "2.4.0" # x-release-please-version +__version__ = "3.0.0-rc1" # x-release-please-version From 164a161cf98db024a95dffa6912e1ed9d7fdddda Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:39:30 +0000 Subject: [PATCH 387/399] perf(client): optimize file structure copying in multipart requests --- src/writerai/_files.py | 56 ++++++++++++++++++- src/writerai/_utils/__init__.py | 1 - src/writerai/_utils/_utils.py | 15 ----- tests/test_deepcopy.py | 58 ------------------- tests/test_files.py | 99 ++++++++++++++++++++++++++++++++- 5 files changed, 151 insertions(+), 78 deletions(-) delete mode 100644 tests/test_deepcopy.py diff --git a/src/writerai/_files.py b/src/writerai/_files.py index cc14c14f..0fdce17b 100644 --- a/src/writerai/_files.py +++ b/src/writerai/_files.py @@ -3,8 +3,8 @@ import io import os import pathlib -from typing import overload -from typing_extensions import TypeGuard +from typing import Sequence, cast, overload +from typing_extensions import TypeVar, TypeGuard import anyio @@ -17,7 +17,9 @@ HttpxFileContent, HttpxRequestFiles, ) -from ._utils import is_tuple_t, is_mapping_t, is_sequence_t +from ._utils import is_list, is_mapping, is_tuple_t, is_mapping_t, is_sequence_t + +_T = TypeVar("_T") def is_base64_file_input(obj: object) -> TypeGuard[Base64FileInput]: @@ -121,3 +123,51 @@ async def async_read_file_content(file: FileContent) -> HttpxFileContent: return await anyio.Path(file).read_bytes() return file + + +def deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]]) -> _T: + """Copy only the containers along the given paths. + + Used to guard against mutation by extract_files without copying the entire structure. + Only dicts and lists that lie on a path are copied; everything else + is returned by reference. + + For example, given paths=[["foo", "files", "file"]] and the structure: + { + "foo": { + "bar": {"baz": {}}, + "files": {"file": } + } + } + The root dict, "foo", and "files" are copied (they lie on the path). + "bar" and "baz" are returned by reference (off the path). + """ + return _deepcopy_with_paths(item, paths, 0) + + +def _deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]], index: int) -> _T: + if not paths: + return item + if is_mapping(item): + key_to_paths: dict[str, list[Sequence[str]]] = {} + for path in paths: + if index < len(path): + key_to_paths.setdefault(path[index], []).append(path) + + # if no path continues through this mapping, it won't be mutated and copying it is redundant + if not key_to_paths: + return item + + result = dict(item) + for key, subpaths in key_to_paths.items(): + if key in result: + result[key] = _deepcopy_with_paths(result[key], subpaths, index + 1) + return cast(_T, result) + if is_list(item): + array_paths = [path for path in paths if index < len(path) and path[index] == ""] + + # if no path expects a list here, nothing will be mutated inside it - return by reference + if not array_paths: + return cast(_T, item) + return cast(_T, [_deepcopy_with_paths(entry, array_paths, index + 1) for entry in item]) + return item diff --git a/src/writerai/_utils/__init__.py b/src/writerai/_utils/__init__.py index 10cb66d2..1c090e51 100644 --- a/src/writerai/_utils/__init__.py +++ b/src/writerai/_utils/__init__.py @@ -24,7 +24,6 @@ coerce_integer as coerce_integer, file_from_path as file_from_path, strip_not_given as strip_not_given, - deepcopy_minimal as deepcopy_minimal, get_async_library as get_async_library, maybe_coerce_float as maybe_coerce_float, get_required_header as get_required_header, diff --git a/src/writerai/_utils/_utils.py b/src/writerai/_utils/_utils.py index 63b8cd60..771859f5 100644 --- a/src/writerai/_utils/_utils.py +++ b/src/writerai/_utils/_utils.py @@ -177,21 +177,6 @@ def is_iterable(obj: object) -> TypeGuard[Iterable[object]]: return isinstance(obj, Iterable) -def deepcopy_minimal(item: _T) -> _T: - """Minimal reimplementation of copy.deepcopy() that will only copy certain object types: - - - mappings, e.g. `dict` - - list - - This is done for performance reasons. - """ - if is_mapping(item): - return cast(_T, {k: deepcopy_minimal(v) for k, v in item.items()}) - if is_list(item): - return cast(_T, [deepcopy_minimal(entry) for entry in item]) - return item - - # copied from https://github.com/Rapptz/RoboDanny def human_join(seq: Sequence[str], *, delim: str = ", ", final: str = "or") -> str: size = len(seq) diff --git a/tests/test_deepcopy.py b/tests/test_deepcopy.py deleted file mode 100644 index 51522e9b..00000000 --- a/tests/test_deepcopy.py +++ /dev/null @@ -1,58 +0,0 @@ -from writerai._utils import deepcopy_minimal - - -def assert_different_identities(obj1: object, obj2: object) -> None: - assert obj1 == obj2 - assert id(obj1) != id(obj2) - - -def test_simple_dict() -> None: - obj1 = {"foo": "bar"} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - - -def test_nested_dict() -> None: - obj1 = {"foo": {"bar": True}} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert_different_identities(obj1["foo"], obj2["foo"]) - - -def test_complex_nested_dict() -> None: - obj1 = {"foo": {"bar": [{"hello": "world"}]}} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert_different_identities(obj1["foo"], obj2["foo"]) - assert_different_identities(obj1["foo"]["bar"], obj2["foo"]["bar"]) - assert_different_identities(obj1["foo"]["bar"][0], obj2["foo"]["bar"][0]) - - -def test_simple_list() -> None: - obj1 = ["a", "b", "c"] - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - - -def test_nested_list() -> None: - obj1 = ["a", [1, 2, 3]] - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert_different_identities(obj1[1], obj2[1]) - - -class MyObject: ... - - -def test_ignores_other_types() -> None: - # custom classes - my_obj = MyObject() - obj1 = {"foo": my_obj} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert obj1["foo"] is my_obj - - # tuples - obj3 = ("a", "b") - obj4 = deepcopy_minimal(obj3) - assert obj3 is obj4 diff --git a/tests/test_files.py b/tests/test_files.py index 9cc66fe2..c34fd9ff 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -4,7 +4,8 @@ import pytest from dirty_equals import IsDict, IsList, IsBytes, IsTuple -from writerai._files import to_httpx_files, async_to_httpx_files +from writerai._files import to_httpx_files, deepcopy_with_paths, async_to_httpx_files +from writerai._utils import extract_files readme_path = Path(__file__).parent.parent.joinpath("README.md") @@ -49,3 +50,99 @@ def test_string_not_allowed() -> None: "file": "foo", # type: ignore } ) + + +def assert_different_identities(obj1: object, obj2: object) -> None: + assert obj1 == obj2 + assert obj1 is not obj2 + + +class TestDeepcopyWithPaths: + def test_copies_top_level_dict(self) -> None: + original = {"file": b"data", "other": "value"} + result = deepcopy_with_paths(original, [["file"]]) + assert_different_identities(result, original) + + def test_file_value_is_same_reference(self) -> None: + file_bytes = b"contents" + original = {"file": file_bytes} + result = deepcopy_with_paths(original, [["file"]]) + assert_different_identities(result, original) + assert result["file"] is file_bytes + + def test_list_popped_wholesale(self) -> None: + files = [b"f1", b"f2"] + original = {"files": files, "title": "t"} + result = deepcopy_with_paths(original, [["files", ""]]) + assert_different_identities(result, original) + result_files = result["files"] + assert isinstance(result_files, list) + assert_different_identities(result_files, files) + + def test_nested_array_path_copies_list_and_elements(self) -> None: + elem1 = {"file": b"f1", "extra": 1} + elem2 = {"file": b"f2", "extra": 2} + original = {"items": [elem1, elem2]} + result = deepcopy_with_paths(original, [["items", "", "file"]]) + assert_different_identities(result, original) + result_items = result["items"] + assert isinstance(result_items, list) + assert_different_identities(result_items, original["items"]) + assert_different_identities(result_items[0], elem1) + assert_different_identities(result_items[1], elem2) + + def test_empty_paths_returns_same_object(self) -> None: + original = {"foo": "bar"} + result = deepcopy_with_paths(original, []) + assert result is original + + def test_multiple_paths(self) -> None: + f1 = b"file1" + f2 = b"file2" + original = {"a": f1, "b": f2, "c": "unchanged"} + result = deepcopy_with_paths(original, [["a"], ["b"]]) + assert_different_identities(result, original) + assert result["a"] is f1 + assert result["b"] is f2 + assert result["c"] is original["c"] + + def test_extract_files_does_not_mutate_original_top_level(self) -> None: + file_bytes = b"contents" + original = {"file": file_bytes, "other": "value"} + + copied = deepcopy_with_paths(original, [["file"]]) + extracted = extract_files(copied, paths=[["file"]]) + + assert extracted == [("file", file_bytes)] + assert original == {"file": file_bytes, "other": "value"} + assert copied == {"other": "value"} + + def test_extract_files_does_not_mutate_original_nested_array_path(self) -> None: + file1 = b"f1" + file2 = b"f2" + original = { + "items": [ + {"file": file1, "extra": 1}, + {"file": file2, "extra": 2}, + ], + "title": "example", + } + + copied = deepcopy_with_paths(original, [["items", "", "file"]]) + extracted = extract_files(copied, paths=[["items", "", "file"]]) + + assert extracted == [("items[][file]", file1), ("items[][file]", file2)] + assert original == { + "items": [ + {"file": file1, "extra": 1}, + {"file": file2, "extra": 2}, + ], + "title": "example", + } + assert copied == { + "items": [ + {"extra": 1}, + {"extra": 2}, + ], + "title": "example", + } From 54ba43a06fb6c9da227fd2c496d94e3d387316a0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 19:55:57 +0000 Subject: [PATCH 388/399] chore(tests): bump steady to v0.22.1 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 5cd7c157..feebe5ed 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.20.2 -- steady --version + npm exec --package=@stdy/cli@0.22.1 -- steady --version - npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.22.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.22.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index b8143aa3..19acc916 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.20.2 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.22.1 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" echo exit 1 From bfb272040d73d8abc5e0c42d25a548639ce62986 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 13:59:30 +0000 Subject: [PATCH 389/399] chore(internal): more robust bootstrap script --- scripts/bootstrap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/bootstrap b/scripts/bootstrap index b430fee3..fe8451e4 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -4,7 +4,7 @@ set -e cd "$(dirname "$0")/.." -if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then +if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "${SKIP_BREW:-}" != "1" ] && [ -t 0 ]; then brew bundle check >/dev/null 2>&1 || { echo -n "==> Install Homebrew dependencies? (y/N): " read -r response From 86b40749d2e84132e61bc92e12f48eac5b4d367e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 21:46:32 +0000 Subject: [PATCH 390/399] fix: use correct field name format for multipart file arrays --- src/writerai/_qs.py | 8 ++----- src/writerai/_types.py | 3 +++ src/writerai/_utils/_utils.py | 42 ++++++++++++++++++++++++++++------- tests/test_extract_files.py | 28 ++++++++++++++++++----- tests/test_files.py | 2 +- 5 files changed, 63 insertions(+), 20 deletions(-) diff --git a/src/writerai/_qs.py b/src/writerai/_qs.py index de8c99bc..4127c19c 100644 --- a/src/writerai/_qs.py +++ b/src/writerai/_qs.py @@ -2,17 +2,13 @@ from typing import Any, List, Tuple, Union, Mapping, TypeVar from urllib.parse import parse_qs, urlencode -from typing_extensions import Literal, get_args +from typing_extensions import get_args -from ._types import NotGiven, not_given +from ._types import NotGiven, ArrayFormat, NestedFormat, not_given from ._utils import flatten _T = TypeVar("_T") - -ArrayFormat = Literal["comma", "repeat", "indices", "brackets"] -NestedFormat = Literal["dots", "brackets"] - PrimitiveData = Union[str, int, float, bool, None] # this should be Data = Union[PrimitiveData, "List[Data]", "Tuple[Data]", "Mapping[str, Data]"] # https://github.com/microsoft/pyright/issues/3555 diff --git a/src/writerai/_types.py b/src/writerai/_types.py index 44e94d72..2a661e32 100644 --- a/src/writerai/_types.py +++ b/src/writerai/_types.py @@ -47,6 +47,9 @@ ModelT = TypeVar("ModelT", bound=pydantic.BaseModel) _T = TypeVar("_T") +ArrayFormat = Literal["comma", "repeat", "indices", "brackets"] +NestedFormat = Literal["dots", "brackets"] + # Approximates httpx internal ProxiesTypes and RequestFiles types # while adding support for `PathLike` instances diff --git a/src/writerai/_utils/_utils.py b/src/writerai/_utils/_utils.py index 771859f5..199cd231 100644 --- a/src/writerai/_utils/_utils.py +++ b/src/writerai/_utils/_utils.py @@ -17,11 +17,11 @@ ) from pathlib import Path from datetime import date, datetime -from typing_extensions import TypeGuard +from typing_extensions import TypeGuard, get_args import sniffio -from .._types import Omit, NotGiven, FileTypes, HeadersLike +from .._types import Omit, NotGiven, FileTypes, ArrayFormat, HeadersLike _T = TypeVar("_T") _TupleT = TypeVar("_TupleT", bound=Tuple[object, ...]) @@ -40,25 +40,45 @@ def extract_files( query: Mapping[str, object], *, paths: Sequence[Sequence[str]], + array_format: ArrayFormat = "brackets", ) -> list[tuple[str, FileTypes]]: """Recursively extract files from the given dictionary based on specified paths. A path may look like this ['foo', 'files', '', 'data']. + ``array_format`` controls how ```` segments contribute to the emitted + field name. Supported values: ``"brackets"`` (``foo[]``), ``"repeat"`` and + ``"comma"`` (``foo``), ``"indices"`` (``foo[0]``, ``foo[1]``). + Note: this mutates the given dictionary. """ files: list[tuple[str, FileTypes]] = [] for path in paths: - files.extend(_extract_items(query, path, index=0, flattened_key=None)) + files.extend(_extract_items(query, path, index=0, flattened_key=None, array_format=array_format)) return files +def _array_suffix(array_format: ArrayFormat, array_index: int) -> str: + if array_format == "brackets": + return "[]" + if array_format == "indices": + return f"[{array_index}]" + if array_format == "repeat" or array_format == "comma": + # Both repeat the bare field name for each file part; there is no + # meaningful way to comma-join binary parts. + return "" + raise NotImplementedError( + f"Unknown array_format value: {array_format}, choose from {', '.join(get_args(ArrayFormat))}" + ) + + def _extract_items( obj: object, path: Sequence[str], *, index: int, flattened_key: str | None, + array_format: ArrayFormat, ) -> list[tuple[str, FileTypes]]: try: key = path[index] @@ -75,9 +95,11 @@ def _extract_items( if is_list(obj): files: list[tuple[str, FileTypes]] = [] - for entry in obj: - assert_is_file_content(entry, key=flattened_key + "[]" if flattened_key else "") - files.append((flattened_key + "[]", cast(FileTypes, entry))) + for array_index, entry in enumerate(obj): + suffix = _array_suffix(array_format, array_index) + emitted_key = (flattened_key + suffix) if flattened_key else suffix + assert_is_file_content(entry, key=emitted_key) + files.append((emitted_key, cast(FileTypes, entry))) return files assert_is_file_content(obj, key=flattened_key) @@ -106,6 +128,7 @@ def _extract_items( path, index=index, flattened_key=flattened_key, + array_format=array_format, ) elif is_list(obj): if key != "": @@ -117,9 +140,12 @@ def _extract_items( item, path, index=index, - flattened_key=flattened_key + "[]" if flattened_key is not None else "[]", + flattened_key=( + (flattened_key if flattened_key is not None else "") + _array_suffix(array_format, array_index) + ), + array_format=array_format, ) - for item in obj + for array_index, item in enumerate(obj) ] ) diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py index 436354bf..0ad3886c 100644 --- a/tests/test_extract_files.py +++ b/tests/test_extract_files.py @@ -4,7 +4,7 @@ import pytest -from writerai._types import FileTypes +from writerai._types import FileTypes, ArrayFormat from writerai._utils import extract_files @@ -37,10 +37,7 @@ def test_multiple_files() -> None: def test_top_level_file_array() -> None: query = {"files": [b"file one", b"file two"], "title": "hello"} - assert extract_files(query, paths=[["files", ""]]) == [ - ("files[]", b"file one"), - ("files[]", b"file two"), - ] + assert extract_files(query, paths=[["files", ""]]) == [("files[]", b"file one"), ("files[]", b"file two")] assert query == {"title": "hello"} @@ -71,3 +68,24 @@ def test_ignores_incorrect_paths( expected: list[tuple[str, FileTypes]], ) -> None: assert extract_files(query, paths=paths) == expected + + +@pytest.mark.parametrize( + "array_format,expected_top_level,expected_nested", + [ + ("brackets", [("files[]", b"a"), ("files[]", b"b")], [("items[][file]", b"a"), ("items[][file]", b"b")]), + ("repeat", [("files", b"a"), ("files", b"b")], [("items[file]", b"a"), ("items[file]", b"b")]), + ("comma", [("files", b"a"), ("files", b"b")], [("items[file]", b"a"), ("items[file]", b"b")]), + ("indices", [("files[0]", b"a"), ("files[1]", b"b")], [("items[0][file]", b"a"), ("items[1][file]", b"b")]), + ], +) +def test_array_format_controls_file_field_names( + array_format: ArrayFormat, + expected_top_level: list[tuple[str, FileTypes]], + expected_nested: list[tuple[str, FileTypes]], +) -> None: + top_level = {"files": [b"a", b"b"]} + assert extract_files(top_level, paths=[["files", ""]], array_format=array_format) == expected_top_level + + nested = {"items": [{"file": b"a"}, {"file": b"b"}]} + assert extract_files(nested, paths=[["items", "", "file"]], array_format=array_format) == expected_nested diff --git a/tests/test_files.py b/tests/test_files.py index c34fd9ff..e37bcded 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -131,7 +131,7 @@ def test_extract_files_does_not_mutate_original_nested_array_path(self) -> None: copied = deepcopy_with_paths(original, [["items", "", "file"]]) extracted = extract_files(copied, paths=[["items", "", "file"]]) - assert extracted == [("items[][file]", file1), ("items[][file]", file2)] + assert [entry for _, entry in extracted] == [file1, file2] assert original == { "items": [ {"file": file1, "extra": 1}, From 24a68a5246a5ede855bc9dd07e1197f34dec2bbf Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 22:35:21 +0000 Subject: [PATCH 391/399] feat: support setting headers via env --- src/writerai/_client.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/writerai/_client.py b/src/writerai/_client.py index 29b42d83..b6cc406d 100644 --- a/src/writerai/_client.py +++ b/src/writerai/_client.py @@ -19,7 +19,11 @@ RequestOptions, not_given, ) -from ._utils import is_given, get_async_library +from ._utils import ( + is_given, + is_mapping_t, + get_async_library, +) from ._compat import cached_property from ._version import __version__ from ._streaming import Stream as Stream, AsyncStream as AsyncStream @@ -89,6 +93,15 @@ def __init__( if base_url is None: base_url = f"https://api.writer.com" + custom_headers_env = os.environ.get("WRITER_CUSTOM_HEADERS") + if custom_headers_env is not None: + parsed: dict[str, str] = {} + for line in custom_headers_env.split("\n"): + colon = line.find(":") + if colon >= 0: + parsed[line[:colon].strip()] = line[colon + 1 :].strip() + default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})} + super().__init__( version=__version__, base_url=base_url, @@ -313,6 +326,15 @@ def __init__( if base_url is None: base_url = f"https://api.writer.com" + custom_headers_env = os.environ.get("WRITER_CUSTOM_HEADERS") + if custom_headers_env is not None: + parsed: dict[str, str] = {} + for line in custom_headers_env.split("\n"): + colon = line.find(":") + if colon >= 0: + parsed[line[:colon].strip()] = line[colon + 1 :].strip() + default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})} + super().__init__( version=__version__, base_url=base_url, From c1979609fcc8f222c1d2dbda6bcfbdb3c3dd8fd2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 18:19:17 +0000 Subject: [PATCH 392/399] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 18b117f4..aaa8b768 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-ea6b4de3976794a02ea8fc01669d901cd7b159ba0d598cc9653e01c987a2f806.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai/writer-ea6b4de3976794a02ea8fc01669d901cd7b159ba0d598cc9653e01c987a2f806.yml openapi_spec_hash: 4d4a9ba232d19a6180e6d4a7d5566103 config_hash: 8701b1a467238f1afdeceeb7feb1adc6 From 9db0d479567ad9cfee40d2e59a23c2cdc45cb367 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:28:06 +0000 Subject: [PATCH 393/399] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index aaa8b768..45b858e1 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai/writer-ea6b4de3976794a02ea8fc01669d901cd7b159ba0d598cc9653e01c987a2f806.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai/writer-275de8f7afa2d37404ebebc082dda35e70ab94437de270b5bc6e2fdc94c9fdae.yml openapi_spec_hash: 4d4a9ba232d19a6180e6d4a7d5566103 config_hash: 8701b1a467238f1afdeceeb7feb1adc6 From 4772bc7528486f23adf744bef7c8f6cff6a6630b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 21:03:11 +0000 Subject: [PATCH 394/399] chore(internal): reformat pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 72168545..1628eef3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -168,7 +168,7 @@ show_error_codes = true # # We also exclude our `tests` as mypy doesn't always infer # types correctly and Pyright will still catch any type errors. -exclude = ['src/writerai/_files.py', '_dev/.*.py', 'tests/.*'] +exclude = ["src/writerai/_files.py", "_dev/.*.py", "tests/.*"] strict_equality = true implicit_reexport = true From 962886ee25fbc8c2563628c9bc343d3b4de95b85 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 16:36:47 +0000 Subject: [PATCH 395/399] fix(client): add missing f-string prefix in file type error message --- src/writerai/_files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/writerai/_files.py b/src/writerai/_files.py index 0fdce17b..76da9e08 100644 --- a/src/writerai/_files.py +++ b/src/writerai/_files.py @@ -99,7 +99,7 @@ async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles elif is_sequence_t(files): files = [(key, await _async_transform_file(file)) for key, file in files] else: - raise TypeError("Unexpected file type input {type(files)}, expected mapping or sequence") + raise TypeError(f"Unexpected file type input {type(files)}, expected mapping or sequence") return files From 2caaa25cb3f9401e4a3689c37e122a16608134ff Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 17:44:38 +0000 Subject: [PATCH 396/399] feat(internal/types): support eagerly validating pydantic iterators --- src/writerai/_models.py | 80 +++++++++++++++++++++++++++++++++++++++++ tests/test_models.py | 60 +++++++++++++++++++++++++++++-- 2 files changed, 137 insertions(+), 3 deletions(-) diff --git a/src/writerai/_models.py b/src/writerai/_models.py index 29070e05..8c5ab260 100644 --- a/src/writerai/_models.py +++ b/src/writerai/_models.py @@ -25,7 +25,9 @@ ClassVar, Protocol, Required, + Annotated, ParamSpec, + TypeAlias, TypedDict, TypeGuard, final, @@ -79,7 +81,15 @@ from ._constants import RAW_RESPONSE_HEADER if TYPE_CHECKING: + from pydantic import GetCoreSchemaHandler, ValidatorFunctionWrapHandler + from pydantic_core import CoreSchema, core_schema from pydantic_core.core_schema import ModelField, ModelSchema, LiteralSchema, ModelFieldsSchema +else: + try: + from pydantic_core import CoreSchema, core_schema + except ImportError: + CoreSchema = None + core_schema = None __all__ = ["BaseModel", "GenericModel"] @@ -396,6 +406,76 @@ def model_dump_json( ) +class _EagerIterable(list[_T], Generic[_T]): + """ + Accepts any Iterable[T] input (including generators), consumes it + eagerly, and validates all items upfront. + + Validation preserves the original container type where possible + (e.g. a set[T] stays a set[T]). Serialization (model_dump / JSON) + always emits a list — round-tripping through model_dump() will not + restore the original container type. + """ + + @classmethod + def __get_pydantic_core_schema__( + cls, + source_type: Any, + handler: GetCoreSchemaHandler, + ) -> CoreSchema: + (item_type,) = get_args(source_type) or (Any,) + item_schema: CoreSchema = handler.generate_schema(item_type) + list_of_items_schema: CoreSchema = core_schema.list_schema(item_schema) + + return core_schema.no_info_wrap_validator_function( + cls._validate, + list_of_items_schema, + serialization=core_schema.plain_serializer_function_ser_schema( + cls._serialize, + info_arg=False, + ), + ) + + @staticmethod + def _validate(v: Iterable[_T], handler: "ValidatorFunctionWrapHandler") -> Any: + original_type: type[Any] = type(v) + + # Normalize to list so list_schema can validate each item + if isinstance(v, list): + items: list[_T] = v + else: + try: + items = list(v) + except TypeError as e: + raise TypeError("Value is not iterable") from e + + # Validate items against the inner schema + validated: list[_T] = handler(items) + + # Reconstruct original container type + if original_type is list: + return validated + # str(list) produces the list's repr, not a string built from items, + # so skip reconstruction for str and its subclasses. + if issubclass(original_type, str): + return validated + try: + return original_type(validated) + except (TypeError, ValueError): + # If the type cannot be reconstructed, just return the validated list + return validated + + @staticmethod + def _serialize(v: Iterable[_T]) -> list[_T]: + """Always serialize as a list so Pydantic's JSON encoder is happy.""" + if isinstance(v, list): + return v + return list(v) + + +EagerIterable: TypeAlias = Annotated[Iterable[_T], _EagerIterable] + + def _construct_field(value: object, field: FieldInfo, key: str) -> object: if value is None: return field_get_default(field) diff --git a/tests/test_models.py b/tests/test_models.py index d5169d03..5be5d2fc 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,7 +1,8 @@ import json -from typing import TYPE_CHECKING, Any, Dict, List, Union, Optional, cast +from typing import TYPE_CHECKING, Any, Dict, List, Union, Iterable, Optional, cast from datetime import datetime, timezone -from typing_extensions import Literal, Annotated, TypeAliasType +from collections import deque +from typing_extensions import Literal, Annotated, TypedDict, TypeAliasType import pytest import pydantic @@ -9,7 +10,7 @@ from writerai._utils import PropertyInfo from writerai._compat import PYDANTIC_V1, parse_obj, model_dump, model_json -from writerai._models import DISCRIMINATOR_CACHE, BaseModel, construct_type +from writerai._models import DISCRIMINATOR_CACHE, BaseModel, EagerIterable, construct_type class BasicModel(BaseModel): @@ -961,3 +962,56 @@ def __getattr__(self, attr: str) -> Item: ... assert model.a.prop == 1 assert isinstance(model.a, Item) assert model.other == "foo" + + +# NOTE: Workaround for Pydantic Iterable behavior. +# Iterable fields are replaced with a ValidatorIterator and may be consumed +# during serialization, which can cause subsequent dumps to return empty data. +# See: https://github.com/pydantic/pydantic/issues/9541 +@pytest.mark.parametrize( + "data, expected_validated", + [ + ([1, 2, 3], [1, 2, 3]), + ((1, 2, 3), (1, 2, 3)), + (set([1, 2, 3]), set([1, 2, 3])), + (iter([1, 2, 3]), [1, 2, 3]), + ([], []), + ((x for x in [1, 2, 3]), [1, 2, 3]), + (map(lambda x: x, [1, 2, 3]), [1, 2, 3]), + (frozenset([1, 2, 3]), frozenset([1, 2, 3])), + (deque([1, 2, 3]), deque([1, 2, 3])), + ], + ids=["list", "tuple", "set", "iterator", "empty", "generator", "map", "frozenset", "deque"], +) +@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2") +def test_iterable_construction(data: Iterable[int], expected_validated: Iterable[int]) -> None: + class TypeWithIterable(TypedDict): + items: EagerIterable[int] + + class Model(BaseModel): + data: TypeWithIterable + + m = Model.model_validate({"data": {"items": data}}) + assert m.data["items"] == expected_validated + + # Verify repeated dumps don't lose data (the original bug) + assert m.model_dump()["data"]["items"] == list(expected_validated) + assert m.model_dump()["data"]["items"] == list(expected_validated) + + +@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2") +def test_iterable_construction_str_falls_back_to_list() -> None: + # str is iterable (over chars), but str(list_of_chars) produces the list's repr + # rather than reconstructing a string from items. We special-case str to fall + # back to list instead of attempting reconstruction. + class TypeWithIterable(TypedDict): + items: EagerIterable[str] + + class Model(BaseModel): + data: TypeWithIterable + + m = Model.model_validate({"data": {"items": "hello"}}) + + # falls back to list of chars rather than calling str(["h", "e", "l", "l", "o"]) + assert m.data["items"] == ["h", "e", "l", "l", "o"] + assert m.model_dump()["data"]["items"] == ["h", "e", "l", "l", "o"] From 6966b7c96f151ebe469b31d2a2202f65b1e35f53 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 19:12:51 +0000 Subject: [PATCH 397/399] ci: pin GitHub Actions to commit SHAs Pin all GitHub Actions referenced in generated workflows (both first-party `actions/*` and third-party) to immutable commit SHAs. Updating pinned actions is now a deliberate codegen-side bump rather than implicit on every workflow run. --- .github/workflows/ci.yml | 8 ++++---- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/release-doctor.yml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd0e630e..a3ffd9a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/writer-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rye run: | @@ -46,7 +46,7 @@ jobs: id-token: write runs-on: ${{ github.repository == 'stainless-sdks/writer-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rye run: | @@ -67,7 +67,7 @@ jobs: github.repository == 'stainless-sdks/writer-python' && !startsWith(github.ref, 'refs/heads/stl/') id: github-oidc - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: core.setOutput('github_token', await core.getIDToken()); @@ -87,7 +87,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/writer-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rye run: | diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index a150e406..8a772966 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rye run: | diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index 4a6bb93d..e797ffe1 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -12,7 +12,7 @@ jobs: if: github.repository == 'writer/writer-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Check release environment run: | From ccdd9109efb18f803a02f22ef1bf51b05639a223 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 20:16:25 +0000 Subject: [PATCH 398/399] fix: treat text/plan with format: binary as raw upload --- api.md | 2 +- src/writerai/resources/files.py | 26 +++++++++++++++++++----- src/writerai/types/file_upload_params.py | 3 --- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/api.md b/api.md index cd8c8e0e..7377b2f1 100644 --- a/api.md +++ b/api.md @@ -155,7 +155,7 @@ Methods: - client.files.delete(file_id) -> FileDeleteResponse - client.files.download(file_id) -> BinaryAPIResponse - client.files.retry(\*\*params) -> FileRetryResponse -- client.files.upload(\*\*params) -> File +- client.files.upload(content, \*\*params) -> File # Tools diff --git a/src/writerai/resources/files.py b/src/writerai/resources/files.py index 0741a674..a7e52e5c 100644 --- a/src/writerai/resources/files.py +++ b/src/writerai/resources/files.py @@ -2,12 +2,26 @@ from __future__ import annotations +import os from typing_extensions import Literal import httpx from ..types import file_list_params, file_retry_params, file_upload_params -from .._types import Body, Omit, Query, Headers, NotGiven, FileTypes, SequenceNotStr, omit, not_given +from .._files import read_file_content, async_read_file_content +from .._types import ( + Body, + Omit, + Query, + Headers, + NotGiven, + BinaryTypes, + FileContent, + SequenceNotStr, + AsyncBinaryTypes, + omit, + not_given, +) from .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -270,8 +284,8 @@ def retry( def upload( self, + content: FileContent | BinaryTypes, *, - content: FileTypes, content_disposition: str, graph_id: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -303,9 +317,10 @@ def upload( timeout: Override the client-level default timeout for this request, in seconds """ extra_headers = {"Content-Disposition": content_disposition, **(extra_headers or {})} + extra_headers["Content-Type"] = "text/plain" return self._post( "/v1/files", - body=maybe_transform(content, file_upload_params.FileUploadParams), + content=read_file_content(content) if isinstance(content, os.PathLike) else content, options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -553,8 +568,8 @@ async def retry( async def upload( self, + content: FileContent | AsyncBinaryTypes, *, - content: FileTypes, content_disposition: str, graph_id: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -586,9 +601,10 @@ async def upload( timeout: Override the client-level default timeout for this request, in seconds """ extra_headers = {"Content-Disposition": content_disposition, **(extra_headers or {})} + extra_headers["Content-Type"] = "text/plain" return await self._post( "/v1/files", - body=await async_maybe_transform(content, file_upload_params.FileUploadParams), + content=await async_read_file_content(content) if isinstance(content, os.PathLike) else content, options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, diff --git a/src/writerai/types/file_upload_params.py b/src/writerai/types/file_upload_params.py index 0487e974..0bd8f75e 100644 --- a/src/writerai/types/file_upload_params.py +++ b/src/writerai/types/file_upload_params.py @@ -4,15 +4,12 @@ from typing_extensions import Required, Annotated, TypedDict -from .._types import FileTypes from .._utils import PropertyInfo __all__ = ["FileUploadParams"] class FileUploadParams(TypedDict, total=False): - content: Required[FileTypes] - content_disposition: Required[Annotated[str, PropertyInfo(alias="Content-Disposition")]] graph_id: Annotated[str, PropertyInfo(alias="graphId")] From 516e4dc2ab1a253c0446c3e37cf39882302841ef Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 18:27:38 +0000 Subject: [PATCH 399/399] chore(internal): version bump --- .release-please-manifest.json | 2 +- README.md | 4 ++-- pyproject.toml | 2 +- src/writerai/_version.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 0ec9934d..4191c889 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "3.0.0-rc1" + ".": "3.0.0" } \ No newline at end of file diff --git a/README.md b/README.md index b3542e5f..8be84e69 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ The REST API documentation can be found on [dev.writer.com](https://dev.writer.c ```sh # install from PyPI -pip install '--pre writer-sdk' +pip install writer-sdk ``` ## Usage @@ -98,7 +98,7 @@ You can enable this by installing `aiohttp`: ```sh # install from PyPI -pip install '--pre writer-sdk[aiohttp]' +pip install writer-sdk[aiohttp] ``` Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: diff --git a/pyproject.toml b/pyproject.toml index 1628eef3..229c1a83 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "writer-sdk" -version = "3.0.0-rc1" +version = "3.0.0" description = "The official Python library for the writer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/writerai/_version.py b/src/writerai/_version.py index 26d04ff0..450bd395 100644 --- a/src/writerai/_version.py +++ b/src/writerai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "writerai" -__version__ = "3.0.0-rc1" # x-release-please-version +__version__ = "3.0.0" # x-release-please-version