From a8df16342cee5ed9b5cb568dae3e7ee189873f4b Mon Sep 17 00:00:00 2001 From: "stainless-sdks-internal[bot]" <167585319+stainless-sdks-internal[bot]@users.noreply.github.com> Date: Sun, 13 Oct 2024 23:18:31 +0000 Subject: [PATCH 01/77] Initial commit --- README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..796864f --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# agility-python \ No newline at end of file From ea9d251debb53b27eba88531312709883a42b128 Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Sun, 13 Oct 2024 23:18:52 +0000 Subject: [PATCH 02/77] feat(api): api update --- .devcontainer/Dockerfile | 9 + .devcontainer/devcontainer.json | 40 + .github/workflows/ci.yml | 53 + .gitignore | 16 + .python-version | 1 + .stats.yml | 2 + Brewfile | 2 + CONTRIBUTING.md | 129 ++ LICENSE | 201 ++ README.md | 345 ++- SECURITY.md | 27 + api.md | 163 ++ bin/publish-pypi | 9 + examples/.keep | 4 + mypy.ini | 47 + noxfile.py | 9 + pyproject.toml | 212 ++ requirements-dev.lock | 105 + requirements.lock | 45 + scripts/bootstrap | 19 + scripts/format | 8 + scripts/lint | 12 + scripts/mock | 41 + scripts/test | 59 + scripts/utils/ruffen-docs.py | 167 ++ src/agility/__init__.py | 83 + src/agility/_base_client.py | 2041 +++++++++++++++++ src/agility/_client.py | 532 +++++ src/agility/_compat.py | 219 ++ src/agility/_constants.py | 14 + src/agility/_exceptions.py | 108 + src/agility/_files.py | 123 + src/agility/_models.py | 785 +++++++ src/agility/_qs.py | 150 ++ src/agility/_resource.py | 43 + src/agility/_response.py | 824 +++++++ src/agility/_streaming.py | 333 +++ src/agility/_types.py | 217 ++ src/agility/_utils/__init__.py | 55 + src/agility/_utils/_logs.py | 25 + src/agility/_utils/_proxy.py | 62 + src/agility/_utils/_reflection.py | 42 + src/agility/_utils/_streams.py | 12 + src/agility/_utils/_sync.py | 81 + src/agility/_utils/_transform.py | 382 +++ src/agility/_utils/_typing.py | 120 + src/agility/_utils/_utils.py | 397 ++++ src/agility/_version.py | 4 + src/agility/lib/.keep | 4 + src/agility/py.typed | 0 src/agility/resources/__init__.py | 75 + src/agility/resources/assistants/__init__.py | 33 + .../resources/assistants/access_keys.py | 380 +++ .../resources/assistants/assistants.py | 595 +++++ src/agility/resources/health.py | 135 ++ .../resources/knowledge_bases/__init__.py | 33 + .../knowledge_bases/knowledge_bases.py | 592 +++++ .../knowledge_bases/sources/__init__.py | 33 + .../knowledge_bases/sources/documents.py | 289 +++ .../knowledge_bases/sources/sources.py | 794 +++++++ src/agility/resources/threads/__init__.py | 47 + src/agility/resources/threads/messages.py | 466 ++++ src/agility/resources/threads/runs.py | 487 ++++ src/agility/resources/threads/threads.py | 459 ++++ src/agility/resources/users/__init__.py | 33 + src/agility/resources/users/api_key.py | 241 ++ src/agility/resources/users/users.py | 195 ++ src/agility/types/__init__.py | 20 + src/agility/types/assistant.py | 22 + src/agility/types/assistant_create_params.py | 20 + src/agility/types/assistant_list_params.py | 13 + src/agility/types/assistant_list_response.py | 10 + src/agility/types/assistant_update_params.py | 22 + src/agility/types/assistant_with_config.py | 29 + src/agility/types/assistants/__init__.py | 8 + src/agility/types/assistants/access_key.py | 33 + .../assistants/access_key_create_params.py | 19 + .../assistants/access_key_list_params.py | 13 + .../assistants/access_key_list_response.py | 10 + src/agility/types/health_check_response.py | 11 + .../types/knowledge_base_create_params.py | 114 + .../types/knowledge_base_list_params.py | 13 + .../types/knowledge_base_list_response.py | 10 + .../types/knowledge_base_update_params.py | 114 + .../types/knowledge_base_with_config.py | 127 + src/agility/types/knowledge_bases/__init__.py | 10 + src/agility/types/knowledge_bases/source.py | 68 + .../knowledge_bases/source_create_params.py | 57 + .../knowledge_bases/source_list_params.py | 13 + .../knowledge_bases/source_list_response.py | 10 + .../knowledge_bases/source_status_response.py | 15 + .../knowledge_bases/source_update_params.py | 59 + .../types/knowledge_bases/sources/__init__.py | 7 + .../types/knowledge_bases/sources/document.py | 34 + .../sources/document_list_params.py | 15 + .../sources/document_list_response.py | 10 + src/agility/types/thread.py | 20 + src/agility/types/thread_list_params.py | 13 + src/agility/types/thread_list_response.py | 10 + src/agility/types/threads/__init__.py | 11 + src/agility/types/threads/message.py | 31 + .../types/threads/message_create_params.py | 20 + .../types/threads/message_list_params.py | 13 + .../types/threads/message_list_response.py | 10 + src/agility/types/threads/run.py | 43 + .../types/threads/run_create_params.py | 36 + .../types/threads/run_stream_params.py | 36 + src/agility/types/user.py | 15 + src/agility/types/users/__init__.py | 5 + .../types/users/api_key_retrieve_response.py | 7 + tests/__init__.py | 1 + tests/api_resources/__init__.py | 1 + tests/api_resources/assistants/__init__.py | 1 + .../assistants/test_access_keys.py | 317 +++ .../api_resources/knowledge_bases/__init__.py | 1 + .../knowledge_bases/sources/__init__.py | 1 + .../knowledge_bases/sources/test_documents.py | 258 +++ .../knowledge_bases/test_sources.py | 896 ++++++++ tests/api_resources/test_assistants.py | 474 ++++ tests/api_resources/test_health.py | 72 + tests/api_resources/test_knowledge_bases.py | 487 ++++ tests/api_resources/test_threads.py | 290 +++ tests/api_resources/test_users.py | 98 + tests/api_resources/threads/__init__.py | 1 + tests/api_resources/threads/test_messages.py | 428 ++++ tests/api_resources/threads/test_runs.py | 510 ++++ tests/api_resources/users/__init__.py | 1 + tests/api_resources/users/test_api_key.py | 174 ++ tests/conftest.py | 63 + tests/sample_file.txt | 1 + tests/test_client.py | 1845 +++++++++++++++ tests/test_deepcopy.py | 58 + 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 | 277 +++ tests/test_streaming.py | 248 ++ tests/test_transform.py | 410 ++++ tests/test_utils/test_proxy.py | 23 + tests/test_utils/test_typing.py | 73 + tests/utils.py | 155 ++ 143 files changed, 22740 insertions(+), 1 deletion(-) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .python-version create mode 100644 .stats.yml create mode 100644 Brewfile create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 SECURITY.md create mode 100644 api.md create mode 100644 bin/publish-pypi create mode 100644 examples/.keep create mode 100644 mypy.ini create mode 100644 noxfile.py create mode 100644 pyproject.toml create mode 100644 requirements-dev.lock create mode 100644 requirements.lock create mode 100755 scripts/bootstrap create mode 100755 scripts/format create mode 100755 scripts/lint create mode 100755 scripts/mock create mode 100755 scripts/test create mode 100644 scripts/utils/ruffen-docs.py create mode 100644 src/agility/__init__.py create mode 100644 src/agility/_base_client.py create mode 100644 src/agility/_client.py create mode 100644 src/agility/_compat.py create mode 100644 src/agility/_constants.py create mode 100644 src/agility/_exceptions.py create mode 100644 src/agility/_files.py create mode 100644 src/agility/_models.py create mode 100644 src/agility/_qs.py create mode 100644 src/agility/_resource.py create mode 100644 src/agility/_response.py create mode 100644 src/agility/_streaming.py create mode 100644 src/agility/_types.py create mode 100644 src/agility/_utils/__init__.py create mode 100644 src/agility/_utils/_logs.py create mode 100644 src/agility/_utils/_proxy.py create mode 100644 src/agility/_utils/_reflection.py create mode 100644 src/agility/_utils/_streams.py create mode 100644 src/agility/_utils/_sync.py create mode 100644 src/agility/_utils/_transform.py create mode 100644 src/agility/_utils/_typing.py create mode 100644 src/agility/_utils/_utils.py create mode 100644 src/agility/_version.py create mode 100644 src/agility/lib/.keep create mode 100644 src/agility/py.typed create mode 100644 src/agility/resources/__init__.py create mode 100644 src/agility/resources/assistants/__init__.py create mode 100644 src/agility/resources/assistants/access_keys.py create mode 100644 src/agility/resources/assistants/assistants.py create mode 100644 src/agility/resources/health.py create mode 100644 src/agility/resources/knowledge_bases/__init__.py create mode 100644 src/agility/resources/knowledge_bases/knowledge_bases.py create mode 100644 src/agility/resources/knowledge_bases/sources/__init__.py create mode 100644 src/agility/resources/knowledge_bases/sources/documents.py create mode 100644 src/agility/resources/knowledge_bases/sources/sources.py create mode 100644 src/agility/resources/threads/__init__.py create mode 100644 src/agility/resources/threads/messages.py create mode 100644 src/agility/resources/threads/runs.py create mode 100644 src/agility/resources/threads/threads.py create mode 100644 src/agility/resources/users/__init__.py create mode 100644 src/agility/resources/users/api_key.py create mode 100644 src/agility/resources/users/users.py create mode 100644 src/agility/types/__init__.py create mode 100644 src/agility/types/assistant.py create mode 100644 src/agility/types/assistant_create_params.py create mode 100644 src/agility/types/assistant_list_params.py create mode 100644 src/agility/types/assistant_list_response.py create mode 100644 src/agility/types/assistant_update_params.py create mode 100644 src/agility/types/assistant_with_config.py create mode 100644 src/agility/types/assistants/__init__.py create mode 100644 src/agility/types/assistants/access_key.py create mode 100644 src/agility/types/assistants/access_key_create_params.py create mode 100644 src/agility/types/assistants/access_key_list_params.py create mode 100644 src/agility/types/assistants/access_key_list_response.py create mode 100644 src/agility/types/health_check_response.py create mode 100644 src/agility/types/knowledge_base_create_params.py create mode 100644 src/agility/types/knowledge_base_list_params.py create mode 100644 src/agility/types/knowledge_base_list_response.py create mode 100644 src/agility/types/knowledge_base_update_params.py create mode 100644 src/agility/types/knowledge_base_with_config.py create mode 100644 src/agility/types/knowledge_bases/__init__.py create mode 100644 src/agility/types/knowledge_bases/source.py create mode 100644 src/agility/types/knowledge_bases/source_create_params.py create mode 100644 src/agility/types/knowledge_bases/source_list_params.py create mode 100644 src/agility/types/knowledge_bases/source_list_response.py create mode 100644 src/agility/types/knowledge_bases/source_status_response.py create mode 100644 src/agility/types/knowledge_bases/source_update_params.py create mode 100644 src/agility/types/knowledge_bases/sources/__init__.py create mode 100644 src/agility/types/knowledge_bases/sources/document.py create mode 100644 src/agility/types/knowledge_bases/sources/document_list_params.py create mode 100644 src/agility/types/knowledge_bases/sources/document_list_response.py create mode 100644 src/agility/types/thread.py create mode 100644 src/agility/types/thread_list_params.py create mode 100644 src/agility/types/thread_list_response.py create mode 100644 src/agility/types/threads/__init__.py create mode 100644 src/agility/types/threads/message.py create mode 100644 src/agility/types/threads/message_create_params.py create mode 100644 src/agility/types/threads/message_list_params.py create mode 100644 src/agility/types/threads/message_list_response.py create mode 100644 src/agility/types/threads/run.py create mode 100644 src/agility/types/threads/run_create_params.py create mode 100644 src/agility/types/threads/run_stream_params.py create mode 100644 src/agility/types/user.py create mode 100644 src/agility/types/users/__init__.py create mode 100644 src/agility/types/users/api_key_retrieve_response.py create mode 100644 tests/__init__.py create mode 100644 tests/api_resources/__init__.py create mode 100644 tests/api_resources/assistants/__init__.py create mode 100644 tests/api_resources/assistants/test_access_keys.py create mode 100644 tests/api_resources/knowledge_bases/__init__.py create mode 100644 tests/api_resources/knowledge_bases/sources/__init__.py create mode 100644 tests/api_resources/knowledge_bases/sources/test_documents.py create mode 100644 tests/api_resources/knowledge_bases/test_sources.py create mode 100644 tests/api_resources/test_assistants.py create mode 100644 tests/api_resources/test_health.py create mode 100644 tests/api_resources/test_knowledge_bases.py create mode 100644 tests/api_resources/test_threads.py create mode 100644 tests/api_resources/test_users.py create mode 100644 tests/api_resources/threads/__init__.py create mode 100644 tests/api_resources/threads/test_messages.py create mode 100644 tests/api_resources/threads/test_runs.py create mode 100644 tests/api_resources/users/__init__.py create mode 100644 tests/api_resources/users/test_api_key.py create mode 100644 tests/conftest.py create mode 100644 tests/sample_file.txt create mode 100644 tests/test_client.py create mode 100644 tests/test_deepcopy.py create mode 100644 tests/test_extract_files.py create mode 100644 tests/test_files.py create mode 100644 tests/test_models.py create mode 100644 tests/test_qs.py create mode 100644 tests/test_required_args.py create mode 100644 tests/test_response.py create mode 100644 tests/test_streaming.py create mode 100644 tests/test_transform.py create mode 100644 tests/test_utils/test_proxy.py create mode 100644 tests/test_utils/test_typing.py create mode 100644 tests/utils.py diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..ac9a2e7 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,9 @@ +ARG VARIANT="3.9" +FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} + +USER vscode + +RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.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/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..bbeb30b --- /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 0000000..4029396 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,53 @@ +name: CI +on: + push: + branches: + - main + pull_request: + branches: + - main + - next + +jobs: + lint: + name: lint + runs-on: ubuntu-latest + + + steps: + - uses: actions/checkout@v4 + + - name: Install Rye + run: | + curl -sSf https://rye.astral.sh/get | bash + echo "$HOME/.rye/shims" >> $GITHUB_PATH + env: + RYE_VERSION: '0.35.0' + RYE_INSTALL_OPTION: '--yes' + + - name: Install dependencies + run: rye sync --all-features + + - name: Run lints + run: ./scripts/lint + test: + name: test + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install Rye + run: | + curl -sSf https://rye.astral.sh/get | bash + echo "$HOME/.rye/shims" >> $GITHUB_PATH + env: + RYE_VERSION: '0.35.0' + RYE_INSTALL_OPTION: '--yes' + + - name: Bootstrap + run: ./scripts/bootstrap + + - name: Run tests + run: ./scripts/test + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8779740 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +.prism.log +.vscode +_dev + +__pycache__ +.mypy_cache + +dist + +.venv +.idea + +.env +.envrc +codegen.log +Brewfile.lock.json diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..43077b2 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.9.18 diff --git a/.stats.yml b/.stats.yml new file mode 100644 index 0000000..51df2cf --- /dev/null +++ b/.stats.yml @@ -0,0 +1,2 @@ +configured_endpoints: 38 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-c2935bcab51a9d09489e4486ff6a9ae450dee07fbbd025136a9174febdb91ccc.yml diff --git a/Brewfile b/Brewfile new file mode 100644 index 0000000..492ca37 --- /dev/null +++ b/Brewfile @@ -0,0 +1,2 @@ +brew "rye" + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ee8eff6 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,129 @@ +## Setting up the environment + +### With Rye + +We use [Rye](https://rye.astral.sh/) to manage dependencies because it will automatically provision a Python environment with the expected Python version. To set it up, run: + +```sh +$ ./scripts/bootstrap +``` + +Or [install Rye manually](https://rye.astral.sh/guide/installation/) and run: + +```sh +$ rye sync --all-features +``` + +You can then run scripts using `rye run python script.py` or by activating the virtual environment: + +```sh +$ rye shell +# or manually activate - https://docs.python.org/3/library/venv.html#how-venvs-work +$ source .venv/bin/activate + +# now you can omit the `rye run` prefix +$ python script.py +``` + +### Without Rye + +Alternatively if you don't want to install `Rye`, you can stick with the standard `pip` setup by ensuring you have the Python version specified in `.python-version`, create a virtual environment however you desire and then install dependencies using this command: + +```sh +$ pip install -r requirements-dev.lock +``` + +## Modifying/Adding code + +Most of the SDK is generated code. Modifications to code will be persisted between generations, but may +result in merge conflicts between manual patches and changes from the generator. The generator will never +modify the contents of the `src/agility/lib/` and `examples/` directories. + +## Adding and running examples + +All files in the `examples/` directory are not modified by the generator and can be freely edited or added to. + +```py +# add an example to examples/.py + +#!/usr/bin/env -S rye run python +… +``` + +```sh +$ chmod +x examples/.py +# run the example against your api +$ ./examples/.py +``` + +## Using the repository from source + +If you’d like to use the repository from source, you can either install from git or link to a cloned repository: + +To install via git: + +```sh +$ pip install git+ssh://git@github.com/stainless-sdks/agility-python.git +``` + +Alternatively, you can build from source and install the wheel file: + +Building this package will create two files in the `dist/` directory, a `.tar.gz` containing the source files and a `.whl` that can be used to install the package efficiently. + +To create a distributable version of the library, all you have to do is run this command: + +```sh +$ rye build +# or +$ python -m build +``` + +Then to install: + +```sh +$ pip install ./path-to-wheel-file.whl +``` + +## Running tests + +Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests. + +```sh +# you will need npm installed +$ npx prism mock path/to/your/openapi.yml +``` + +```sh +$ ./scripts/test +``` + +## Linting and formatting + +This repository uses [ruff](https://github.com/astral-sh/ruff) and +[black](https://github.com/psf/black) to format the code in the repository. + +To lint: + +```sh +$ ./scripts/lint +``` + +To format and fix all ruff issues automatically: + +```sh +$ ./scripts/format +``` + +## Publishing and releases + +Changes made to this repository via the automated release PR pipeline should publish to PyPI automatically. If +the changes aren't made through the automated pipeline, you may want to make releases manually. + +### Publish with a GitHub workflow + +You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/stainless-sdks/agility-python/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. + +### Publish manually + +If you need to manually release a package, you can run the `bin/publish-pypi` script with a `PYPI_TOKEN` set on +the environment. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..77211a2 --- /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 Agility + + 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 796864f..ce39246 100644 --- a/README.md +++ b/README.md @@ -1 +1,344 @@ -# agility-python \ No newline at end of file +# Agility Python API library + +[![PyPI version](https://img.shields.io/pypi/v/agility.svg)](https://pypi.org/project/agility/) + +The Agility Python library provides convenient access to the Agility 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 [docs.agility.com](https://docs.agility.com). 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/agility-python.git +``` + +> [!NOTE] +> Once this package is [published to PyPI](https://app.stainlessapi.com/docs/guides/publish), this will become: `pip install --pre agility` + +## Usage + +The full API of this library can be found in [api.md](api.md). + +```python +from agility import Agility + +client = Agility() + +assistant = client.assistants.create( + description="description", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", +) +print(assistant.id) +``` + +While you can provide a `bearer_token` keyword argument, +we recommend using [python-dotenv](https://pypi.org/project/python-dotenv/) +to add `BEARER_TOKEN="My Bearer Token"` to your `.env` file +so that your Bearer Token is not stored in source control. + +## Async usage + +Simply import `AsyncAgility` instead of `Agility` and use `await` with each API call: + +```python +import asyncio +from agility import AsyncAgility + +client = AsyncAgility() + + +async def main() -> None: + assistant = await client.assistants.create( + description="description", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + ) + print(assistant.id) + + +asyncio.run(main()) +``` + +Functionality between the synchronous and asynchronous clients is otherwise identical. + +## Using types + +Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev) which also provide helper methods for things like: + +- Serializing back into JSON, `model.to_json()` +- Converting to a dictionary, `model.to_dict()` + +Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`. + +## 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 `agility.APIConnectionError` is raised. + +When the API returns a non-success status code (that is, 4xx or 5xx +response), a subclass of `agility.APIStatusError` is raised, containing `status_code` and `response` properties. + +All errors inherit from `agility.APIError`. + +```python +import agility +from agility import Agility + +client = Agility() + +try: + client.assistants.create( + description="description", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + ) +except agility.APIConnectionError as e: + print("The server could not be reached") + print(e.__cause__) # an underlying Exception, likely raised within httpx. +except agility.RateLimitError as e: + print("A 429 status code was received; we should back off a bit.") +except agility.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 agility import Agility + +# Configure the default for all requests: +client = Agility( + # default is 2 + max_retries=0, +) + +# Or, configure per-request: +client.with_options(max_retries=5).assistants.create( + description="description", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", +) +``` + +### 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 agility import Agility + +# Configure the default for all requests: +client = Agility( + # 20 seconds (default is 1 minute) + timeout=20.0, +) + +# More granular control: +client = Agility( + timeout=httpx.Timeout(60.0, read=5.0, write=10.0, connect=2.0), +) + +# Override per-request: +client.with_options(timeout=5.0).assistants.create( + description="description", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", +) +``` + +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 `AGILITY_LOG` to `debug`. + +```shell +$ export AGILITY_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 agility import Agility + +client = Agility() +response = client.assistants.with_raw_response.create( + description="description", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", +) +print(response.headers.get('X-My-Header')) + +assistant = response.parse() # get the object that `assistants.create()` would have returned +print(assistant.id) +``` + +These methods return an [`APIResponse`](https://github.com/stainless-sdks/agility-python/tree/main/src/agility/_response.py) object. + +The async client returns an [`AsyncAPIResponse`](https://github.com/stainless-sdks/agility-python/tree/main/src/agility/_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.assistants.with_streaming_response.create( + description="description", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", +) 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/clients/) functionality + +```python +from agility import Agility, DefaultHttpxClient + +client = Agility( + # Or use the `AGILITY_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"), + ), +) +``` + +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. + +## 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/agility-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 agility +print(agility.__version__) +``` + +## Requirements + +Python 3.7 or higher. + +## Contributing + +See [the contributing documentation](./CONTRIBUTING.md). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..c32c806 --- /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 Agility please follow the respective company's security reporting guidelines. + +### Agility Terms and Policies + +Please contact dev-feedback@agility.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/api.md b/api.md new file mode 100644 index 0000000..e2533b5 --- /dev/null +++ b/api.md @@ -0,0 +1,163 @@ +# Assistants + +Types: + +```python +from agility.types import Assistant, AssistantWithConfig, AssistantListResponse +``` + +Methods: + +- client.assistants.create(\*\*params) -> Assistant +- client.assistants.retrieve(assistant_id) -> AssistantWithConfig +- client.assistants.update(assistant_id, \*\*params) -> AssistantWithConfig +- client.assistants.list(\*\*params) -> AssistantListResponse +- client.assistants.delete(assistant_id) -> None + +## AccessKeys + +Types: + +```python +from agility.types.assistants import AccessKey, AccessKeyListResponse +``` + +Methods: + +- client.assistants.access_keys.create(assistant_id, \*\*params) -> AccessKey +- client.assistants.access_keys.retrieve(access_key_id, \*, assistant_id) -> AccessKey +- client.assistants.access_keys.list(assistant_id, \*\*params) -> AccessKeyListResponse + +# KnowledgeBases + +Types: + +```python +from agility.types import KnowledgeBaseWithConfig, KnowledgeBaseListResponse +``` + +Methods: + +- client.knowledge_bases.create(\*\*params) -> KnowledgeBaseWithConfig +- client.knowledge_bases.retrieve(knowledge_base_id) -> KnowledgeBaseWithConfig +- client.knowledge_bases.update(knowledge_base_id, \*\*params) -> KnowledgeBaseWithConfig +- client.knowledge_bases.list(\*\*params) -> KnowledgeBaseListResponse +- client.knowledge_bases.delete(knowledge_base_id) -> None + +## Sources + +Types: + +```python +from agility.types.knowledge_bases import ( + Source, + SourceStatusResponse, + SourceListResponse, + SourceSyncResponse, +) +``` + +Methods: + +- client.knowledge_bases.sources.create(knowledge_base_id, \*\*params) -> Source +- client.knowledge_bases.sources.retrieve(source_id, \*, knowledge_base_id) -> Source +- client.knowledge_bases.sources.update(source_id, \*, knowledge_base_id, \*\*params) -> Source +- client.knowledge_bases.sources.list(knowledge_base_id, \*\*params) -> SourceListResponse +- client.knowledge_bases.sources.delete(source_id, \*, knowledge_base_id) -> None +- client.knowledge_bases.sources.status(source_id, \*, knowledge_base_id) -> SourceStatusResponse +- client.knowledge_bases.sources.sync(source_id, \*, knowledge_base_id) -> object + +### Documents + +Types: + +```python +from agility.types.knowledge_bases.sources import Document, DocumentListResponse +``` + +Methods: + +- client.knowledge_bases.sources.documents.retrieve(document_id, \*, knowledge_base_id, source_id) -> Document +- client.knowledge_bases.sources.documents.list(source_id, \*, knowledge_base_id, \*\*params) -> DocumentListResponse + +# Health + +Types: + +```python +from agility.types import HealthCheckResponse +``` + +Methods: + +- client.health.check() -> HealthCheckResponse + +# Users + +Types: + +```python +from agility.types import User +``` + +Methods: + +- client.users.retrieve(user_id) -> User + +## APIKey + +Types: + +```python +from agility.types.users import APIKeyRetrieveResponse +``` + +Methods: + +- client.users.api_key.retrieve(user_id) -> str +- client.users.api_key.refresh(user_id) -> User + +# Threads + +Types: + +```python +from agility.types import Thread, ThreadListResponse +``` + +Methods: + +- client.threads.create() -> Thread +- client.threads.retrieve(thread_id) -> Thread +- client.threads.list(\*\*params) -> ThreadListResponse +- client.threads.delete(thread_id) -> None + +## Messages + +Types: + +```python +from agility.types.threads import Message, MessageListResponse +``` + +Methods: + +- client.threads.messages.create(thread_id, \*\*params) -> Message +- client.threads.messages.retrieve(message_id, \*, thread_id) -> Message +- client.threads.messages.list(thread_id, \*\*params) -> MessageListResponse +- client.threads.messages.delete(message_id, \*, thread_id) -> None + +## Runs + +Types: + +```python +from agility.types.threads import Run, RunStreamResponse +``` + +Methods: + +- client.threads.runs.create(thread_id, \*\*params) -> Run +- client.threads.runs.retrieve(run_id, \*, thread_id) -> Run +- client.threads.runs.delete(run_id, \*, thread_id) -> None +- client.threads.runs.stream(thread_id, \*\*params) -> object diff --git a/bin/publish-pypi b/bin/publish-pypi new file mode 100644 index 0000000..05bfccb --- /dev/null +++ b/bin/publish-pypi @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +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/examples/.keep b/examples/.keep new file mode 100644 index 0000000..d8c73e9 --- /dev/null +++ b/examples/.keep @@ -0,0 +1,4 @@ +File generated from our OpenAPI spec by Stainless. + +This directory can be used to store example files demonstrating usage of this SDK. +It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. \ No newline at end of file diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..7e0fc28 --- /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/agility/_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 0000000..53bca7f --- /dev/null +++ b/noxfile.py @@ -0,0 +1,9 @@ +import nox + + +@nox.session(reuse_venv=True, name="test-pydantic-v1") +def test_pydantic_v1(session: nox.Session) -> None: + session.install("-r", "requirements-dev.lock") + session.install("pydantic<2") + + session.run("pytest", "--showlocals", "--ignore=tests/functional", *session.posargs) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c5ec7ac --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,212 @@ +[project] +name = "agility" +version = "0.0.1-alpha.0" +description = "The official Python library for the agility API" +dynamic = ["readme"] +license = "Apache-2.0" +authors = [ +{ name = "Agility", email = "dev-feedback@agility.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/agility-python" +Repository = "https://github.com/stainless-sdks/agility-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", + "rich>=13.7.1", +] + +[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:importable", +]} +"check:ruff" = "ruff check ." +"fix:ruff" = "ruff check --fix ." + +"check:importable" = "python -c 'import agility'" + +typecheck = { chain = [ + "typecheck:pyright", + "typecheck:mypy" +]} +"typecheck:pyright" = "pyright" +"typecheck:verify-types" = "pyright --verifytypes agility --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/agility"] + +[tool.hatch.build.targets.sdist] +# Basically everything except hidden files/directories (such as .github, .devcontainers, .python-version, etc) +include = [ + "/*.toml", + "/*.json", + "/*.lock", + "/*.md", + "/mypy.ini", + "/noxfile.py", + "bin/*", + "examples/*", + "src/*", + "tests/*", +] + +[tool.hatch.metadata.hooks.fancy-pypi-readme] +content-type = "text/markdown" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] +path = "README.md" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]] +# replace relative links with absolute links +pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)' +replacement = '[\1](https://github.com/stainless-sdks/agility-python/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" + +[tool.ruff.format] +docstring-code-format = true + +[tool.ruff.lint] +select = [ + # isort + "I", + # bugbear rules + "B", + # remove unused imports + "F401", + # bare except statements + "E722", + # unused arguments + "ARG", + # print statements + "T201", + "T203", + # misuse of typing.TYPE_CHECKING + "TCH004", + # import rules + "TID251", +] +ignore = [ + # mutable defaults + "B006", +] +unfixable = [ + # disable auto fix for print statements + "T201", + "T203", +] + +[tool.ruff.lint.flake8-tidy-imports.banned-api] +"functools.lru_cache".msg = "This function does not retain type information for the wrapped function's arguments; The `lru_cache` function from `_utils` should be used instead" + +[tool.ruff.lint.isort] +length-sort = true +length-sort-straight = true +combine-as-imports = true +extra-standard-library = ["typing_extensions"] +known-first-party = ["agility", "tests"] + +[tool.ruff.lint.per-file-ignores] +"bin/**.py" = ["T201", "T203"] +"scripts/**.py" = ["T201", "T203"] +"tests/**.py" = ["T201", "T203"] +"examples/**.py" = ["T201", "T203"] diff --git a/requirements-dev.lock b/requirements-dev.lock new file mode 100644 index 0000000..0795d4d --- /dev/null +++ b/requirements-dev.lock @@ -0,0 +1,105 @@ +# generated by rye +# use `rye lock` or `rye sync` to update this lockfile +# +# last locked with the following flags: +# pre: false +# features: [] +# all-features: true +# with-sources: false +# generate-hashes: false + +-e file:. +annotated-types==0.6.0 + # via pydantic +anyio==4.4.0 + # via agility + # via httpx +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 agility +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 agility + # via respx +idna==3.4 + # via anyio + # via httpx +importlib-metadata==7.0.0 +iniconfig==2.0.0 + # via pytest +markdown-it-py==3.0.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py +mypy==1.11.2 +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.7.1 + # via agility +pydantic-core==2.18.2 + # via pydantic +pygments==2.18.0 + # via rich +pyright==1.1.380 +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 +rich==13.7.1 +ruff==0.6.5 +setuptools==68.2.2 + # via nodeenv +six==1.16.0 + # via python-dateutil +sniffio==1.3.0 + # via agility + # via anyio + # via httpx +time-machine==2.9.0 +tomli==2.0.1 + # via mypy + # via pytest +typing-extensions==4.8.0 + # via agility + # via anyio + # via mypy + # via pydantic + # via pydantic-core +virtualenv==20.24.5 + # via nox +zipp==3.17.0 + # via importlib-metadata diff --git a/requirements.lock b/requirements.lock new file mode 100644 index 0000000..e3e4e8b --- /dev/null +++ b/requirements.lock @@ -0,0 +1,45 @@ +# generated by rye +# use `rye lock` or `rye sync` to update this lockfile +# +# last locked with the following flags: +# pre: false +# features: [] +# all-features: true +# with-sources: false +# generate-hashes: false + +-e file:. +annotated-types==0.6.0 + # via pydantic +anyio==4.4.0 + # via agility + # via httpx +certifi==2023.7.22 + # via httpcore + # via httpx +distro==1.8.0 + # via agility +exceptiongroup==1.1.3 + # via anyio +h11==0.14.0 + # via httpcore +httpcore==1.0.2 + # via httpx +httpx==0.25.2 + # via agility +idna==3.4 + # via anyio + # via httpx +pydantic==2.7.1 + # via agility +pydantic-core==2.18.2 + # via pydantic +sniffio==1.3.0 + # via agility + # via anyio + # via httpx +typing-extensions==4.8.0 + # via agility + # via anyio + # via pydantic + # via pydantic-core diff --git a/scripts/bootstrap b/scripts/bootstrap new file mode 100755 index 0000000..8c5c60e --- /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 --all-features diff --git a/scripts/format b/scripts/format new file mode 100755 index 0000000..667ec2d --- /dev/null +++ b/scripts/format @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +echo "==> Running formatters" +rye run format diff --git a/scripts/lint b/scripts/lint new file mode 100755 index 0000000..97fd526 --- /dev/null +++ b/scripts/lint @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +echo "==> Running lints" +rye run lint + +echo "==> Making sure it imports" +rye run python -c 'import agility' + diff --git a/scripts/mock b/scripts/mock new file mode 100755 index 0000000..d2814ae --- /dev/null +++ b/scripts/mock @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +if [[ -n "$1" && "$1" != '--'* ]]; then + URL="$1" + shift +else + URL="$(grep 'openapi_spec_url' .stats.yml | cut -d' ' -f2)" +fi + +# Check if the URL is empty +if [ -z "$URL" ]; then + echo "Error: No OpenAPI spec path/url provided or found in .stats.yml" + exit 1 +fi + +echo "==> Starting mock server with URL ${URL}" + +# Run prism mock on the given spec +if [ "$1" == "--daemon" ]; then + npm exec --package=@stainless-api/prism-cli@5.8.5 -- prism mock "$URL" &> .prism.log & + + # Wait for server to come online + echo -n "Waiting for server" + while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do + echo -n "." + sleep 0.1 + done + + if grep -q "✖ fatal" ".prism.log"; then + cat .prism.log + exit 1 + fi + + echo +else + npm exec --package=@stainless-api/prism-cli@5.8.5 -- prism mock "$URL" +fi diff --git a/scripts/test b/scripts/test new file mode 100755 index 0000000..4fa5698 --- /dev/null +++ b/scripts/test @@ -0,0 +1,59 @@ +#!/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 + +echo "==> Running tests" +rye run pytest "$@" + +echo "==> Running Pydantic v1 tests" +rye run nox -s test-pydantic-v1 -- "$@" diff --git a/scripts/utils/ruffen-docs.py b/scripts/utils/ruffen-docs.py new file mode 100644 index 0000000..37b3d94 --- /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/agility/__init__.py b/src/agility/__init__.py new file mode 100644 index 0000000..5569675 --- /dev/null +++ b/src/agility/__init__.py @@ -0,0 +1,83 @@ +# 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, Agility, Timeout, Transport, AsyncClient, AsyncStream, AsyncAgility, 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, + AgilityError, + ConflictError, + NotFoundError, + APIStatusError, + RateLimitError, + APITimeoutError, + BadRequestError, + APIConnectionError, + AuthenticationError, + InternalServerError, + PermissionDeniedError, + UnprocessableEntityError, + APIResponseValidationError, +) +from ._base_client import DefaultHttpxClient, DefaultAsyncHttpxClient +from ._utils._logs import setup_logging as _setup_logging + +__all__ = [ + "types", + "__version__", + "__title__", + "NoneType", + "Transport", + "ProxiesTypes", + "NotGiven", + "NOT_GIVEN", + "AgilityError", + "APIError", + "APIStatusError", + "APITimeoutError", + "APIConnectionError", + "APIResponseValidationError", + "BadRequestError", + "AuthenticationError", + "PermissionDeniedError", + "NotFoundError", + "ConflictError", + "UnprocessableEntityError", + "RateLimitError", + "InternalServerError", + "Timeout", + "RequestOptions", + "Client", + "AsyncClient", + "Stream", + "AsyncStream", + "Agility", + "AsyncAgility", + "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. +# agility._exceptions.NotFoundError -> agility.NotFoundError +__locals = locals() +for __name in __all__: + if not __name.startswith("__"): + try: + __locals[__name].__module__ = "agility" + except (TypeError, AttributeError): + # Some of our exported symbols are builtins which we can't set attributes for. + pass diff --git a/src/agility/_base_client.py b/src/agility/_base_client.py new file mode 100644 index 0000000..f00254b --- /dev/null +++ b/src/agility/_base_client.py @@ -0,0 +1,2041 @@ +from __future__ import annotations + +import sys +import json +import time +import uuid +import email +import asyncio +import inspect +import logging +import platform +import warnings +import email.utils +from types import TracebackType +from random import random +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Type, + Union, + Generic, + Mapping, + TypeVar, + Iterable, + Iterator, + Optional, + Generator, + AsyncIterator, + cast, + overload, +) +from typing_extensions import Literal, override, get_origin + +import anyio +import httpx +import distro +import pydantic +from httpx import URL, 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, + HttpxRequestFiles, + ModelBuilderProtocol, +) +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 ( + 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 + + @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]): + """ + 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 + self._platform: Platform | None = None + + if max_retries is None: # pyright: ignore[reportUnnecessaryComparison] + raise TypeError( + "max_retries cannot be None. If you want to disable retries, pass `0`; if you want unlimited retries, pass `math.inf` or a very high number; if you want the default behavior, pass `agility.DEFAULT_MAX_RETRIES`" + ) + + def _enforce_trailing_slash(self, url: URL) -> URL: + if url.raw_path.endswith(b"/"): + return url + return url.copy_with(raw_path=url.raw_path + b"/") + + def _make_status_error_from_response( + self, + response: httpx.Response, + ) -> APIStatusError: + if response.is_closed and not response.is_stream_consumed: + # We can't read the response body as it has been closed + # before it was read. This can happen if an event hook + # raises a status error. + body = None + err_msg = f"Error code: {response.status_code}" + else: + err_text = response.text.strip() + body = err_text + + try: + body = json.loads(err_text) + err_msg = f"Error code: {response.status_code} - {body}" + except Exception: + err_msg = err_text or f"Error code: {response.status_code}" + + return self._make_status_error(err_msg, body=body, response=response) + + def _make_status_error( + self, + err_msg: str, + *, + body: object, + response: httpx.Response, + ) -> _exceptions.APIStatusError: + raise NotImplementedError() + + def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0) -> httpx.Headers: + custom_headers = options.headers or {} + headers_dict = _merge_mappings(self.default_headers, custom_headers) + self._validate_headers(headers_dict, custom_headers) + + # headers are case-insensitive while dictionaries are not. + headers = httpx.Headers(headers_dict) + + idempotency_header = self._idempotency_header + if idempotency_header and options.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 + # `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 + + def _prepare_url(self, url: str) -> URL: + """ + Merge a URL argument together with any 'base_url' on the client, + to create the URL used for the outgoing request. + """ + # Copied from httpx's `_merge_url` method. + merge_url = URL(url) + if merge_url.is_relative_url: + merge_raw_path = self.base_url.raw_path + merge_url.raw_path.lstrip(b"/") + return self.base_url.copy_with(raw_path=merge_raw_path) + + return merge_url + + def _make_sse_decoder(self) -> SSEDecoder | SSEBytesDecoder: + return SSEDecoder() + + def _build_request( + self, + options: FinalRequestOptions, + *, + retries_taken: int = 0, + ) -> httpx.Request: + if log.isEnabledFor(logging.DEBUG): + log.debug("Request options: %s", model_dump(options, exclude_unset=True)) + + kwargs: dict[str, Any] = {} + + json_data = options.json_data + if options.extra_json is not None: + if json_data is None: + json_data = cast(Body, options.extra_json) + elif is_mapping(json_data): + json_data = _merge_mappings(json_data, options.extra_json) + else: + raise RuntimeError(f"Unexpected JSON data type, {type(json_data)}, cannot merge with `extra_body`") + + headers = self._build_headers(options, retries_taken=retries_taken) + params = _merge_mappings(self.default_query, options.params) + content_type = headers.get("Content-Type") + files = options.files + + # If the given Content-Type header is multipart/form-data then it + # has to be removed so that httpx can generate the header with + # additional information for us as it has to be in this form + # for the server to be able to correctly parse the request: + # multipart/form-data; boundary=---abc-- + if content_type is not None and content_type.startswith("multipart/form-data"): + if "boundary" not in content_type: + # only remove the header if the boundary hasn't been explicitly set + # as the caller doesn't want httpx to come up with their own boundary + headers.pop("Content-Type") + + # As we are now sending multipart/form-data instead of application/json + # we need to tell httpx to use it, https://www.python-httpx.org/advanced/clients/#multipart-file-encoding + if json_data: + if not is_dict(json_data): + raise TypeError( + f"Expected query input to be a dictionary for multipart requests but got {type(json_data)} instead." + ) + kwargs["data"] = self._serialize_multipartform(json_data) + + # httpx determines whether or not to send a "multipart/form-data" + # request based on the truthiness of the "files" argument. + # This gets around that issue by generating a dict value that + # evaluates to true. + # + # https://github.com/encode/httpx/discussions/2399#discussioncomment-3814186 + if not files: + files = cast(HttpxRequestFiles, ForceMultipartDict()) + + prepared_url = self._prepare_url(options.url) + if "_" in prepared_url.host: + # work around https://github.com/encode/httpx/discussions/2880 + kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")} + + # TODO: report this error to httpx + return self._client.build_request( # pyright: ignore[reportUnknownMemberType] + headers=headers, + timeout=self.timeout if isinstance(options.timeout, NotGiven) else options.timeout, + method=options.method, + url=prepared_url, + # the `Query` type that we use is incompatible with qs' + # `Params` type as it needs to be typed as `Mapping[str, object]` + # so that passing a `TypedDict` doesn't cause an error. + # https://github.com/microsoft/pyright/issues/3526#event-6715453066 + params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None, + json=json_data, + files=files, + **kwargs, + ) + + def _serialize_multipartform(self, data: Mapping[object, object]) -> dict[str, object]: + items = self.qs.stringify_items( + # TODO: type ignore is required as stringify_items is well typed but we can't be + # well typed without heavy validation. + data, # type: ignore + array_format="brackets", + ) + serialized: dict[str, object] = {} + for key, value in items: + existing = serialized.get(key) + + if not existing: + serialized[key] = value + continue + + # If a value has already been set for this key then that + # means we're sending data like `array[]=[1, 2, 3]` and we + # need to tell httpx that we want to send multiple values with + # the same key which is done by using a list or a tuple. + # + # Note: 2d arrays should never result in the same key at both + # levels so it's safe to assume that if the value is a list, + # it was because we changed it to be a list. + if is_list(existing): + existing.append(value) + else: + serialized[key] = [existing, value] + + return serialized + + def _maybe_override_cast_to(self, cast_to: type[ResponseT], options: FinalRequestOptions) -> type[ResponseT]: + if not is_given(options.headers): + return cast_to + + # make a copy of the headers so we don't mutate user-input + headers = dict(options.headers) + + # we internally support defining a temporary header to override the + # default `cast_to` type for use with `.with_raw_response` and `.with_streaming_response` + # see _response.py for implementation details + override_cast_to = headers.pop(OVERRIDE_CAST_TO_HEADER, NOT_GIVEN) + if is_given(override_cast_to): + options.headers = headers + return cast(Type[ResponseT], override_cast_to) + + return cast_to + + def _should_stream_response_body(self, request: httpx.Request) -> bool: + return request.headers.get(RAW_RESPONSE_HEADER) == "stream" # type: ignore[no-any-return] + + def _process_response_data( + self, + *, + data: object, + cast_to: type[ResponseT], + response: httpx.Response, + ) -> ResponseT: + if data is None: + return cast(ResponseT, None) + + if cast_to is object: + return cast(ResponseT, data) + + try: + if inspect.isclass(cast_to) and issubclass(cast_to, ModelBuilderProtocol): + return cast(ResponseT, cast_to.build(response=response, data=data)) + + if self._strict_response_validation: + return cast(ResponseT, validate_type(type_=cast_to, value=data)) + + return cast(ResponseT, construct_type(type_=cast_to, value=data)) + except pydantic.ValidationError as err: + raise APIResponseValidationError(response=response, body=data) from err + + @property + def qs(self) -> Querystring: + return Querystring() + + @property + def custom_auth(self) -> httpx.Auth | None: + return None + + @property + def auth_headers(self) -> dict[str, str]: + return {} + + @property + def default_headers(self) -> dict[str, str | Omit]: + return { + "Accept": "application/json", + "Content-Type": "application/json", + "User-Agent": self.user_agent, + **self.platform_headers(), + **self.auth_headers, + **self._custom_headers, + } + + @property + def default_query(self) -> dict[str, object]: + return { + **self._custom_query, + } + + def _validate_headers( + self, + headers: Headers, # noqa: ARG002 + custom_headers: Headers, # noqa: ARG002 + ) -> None: + """Validate the given default headers and custom headers. + + Does nothing by default. + """ + return + + @property + def user_agent(self) -> str: + return f"{self.__class__.__name__}/Python {self._version}" + + @property + def base_url(self) -> URL: + return self._base_url + + @base_url.setter + def base_url(self, url: URL | str) -> None: + self._base_url = self._enforce_trailing_slash(url if isinstance(url, URL) else URL(url)) + + def platform_headers(self) -> Dict[str, str]: + # the actual implementation is in a separate `lru_cache` decorated + # function because adding `lru_cache` to methods will leak memory + # https://github.com/python/cpython/issues/88476 + return platform_headers(self._version, platform=self._platform) + + def _parse_retry_after_header(self, response_headers: Optional[httpx.Headers] = None) -> float | None: + """Returns a float of the number of seconds (not milliseconds) to wait after retrying, or None if unspecified. + + About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After + See also https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After#syntax + """ + if response_headers is None: + return None + + # First, try the non-standard `retry-after-ms` header for milliseconds, + # which is more precise than integer-seconds `retry-after` + try: + retry_ms_header = response_headers.get("retry-after-ms", None) + return float(retry_ms_header) / 1000 + except (TypeError, ValueError): + pass + + # Next, try parsing `retry-after` header as seconds (allowing nonstandard floats). + retry_header = response_headers.get("retry-after") + try: + # note: the spec indicates that this should only ever be an integer + # but if someone sends a float there's no reason for us to not respect it + return float(retry_header) + except (TypeError, ValueError): + pass + + # Last, try parsing `retry-after` as a date. + retry_date_tuple = email.utils.parsedate_tz(retry_header) + if retry_date_tuple is None: + return None + + retry_date = email.utils.mktime_tz(retry_date_tuple) + return float(retry_date - time.time()) + + def _calculate_retry_timeout( + self, + remaining_retries: int, + options: FinalRequestOptions, + response_headers: Optional[httpx.Headers] = None, + ) -> float: + max_retries = options.get_max_retries(self.max_retries) + + # If the API asks us to wait a certain amount of time (and it's a reasonable amount), just do what it says. + retry_after = self._parse_retry_after_header(response_headers) + if retry_after is not None and 0 < retry_after <= 60: + return retry_after + + # Also cap retry count to 1000 to avoid any potential overflows with `pow` + nb_retries = min(max_retries - remaining_retries, 1000) + + # Apply exponential backoff, but not more than the max. + sleep_seconds = min(INITIAL_RETRY_DELAY * pow(2.0, nb_retries), MAX_RETRY_DELAY) + + # Apply some jitter, plus-or-minus half a second. + jitter = 1 - 0.25 * random() + timeout = sleep_seconds * jitter + return timeout if timeout >= 0 else 0 + + def _should_retry(self, response: httpx.Response) -> bool: + # Note: this is not a standard header + should_retry_header = response.headers.get("x-should-retry") + + # If the server explicitly says whether or not to retry, obey. + if should_retry_header == "true": + log.debug("Retrying as header `x-should-retry` is set to `true`") + return True + if should_retry_header == "false": + log.debug("Not retrying as header `x-should-retry` is set to `false`") + return False + + # Retry on request timeouts. + if response.status_code == 408: + log.debug("Retrying due to status code %i", response.status_code) + return True + + # Retry on lock timeouts. + if response.status_code == 409: + log.debug("Retrying due to status code %i", response.status_code) + return True + + # Retry on rate limits. + if response.status_code == 429: + log.debug("Retrying due to status code %i", response.status_code) + return True + + # Retry internal errors. + if response.status_code >= 500: + log.debug("Retrying due to status code %i", response.status_code) + return True + + log.debug("Not retrying") + return False + + def _idempotency_key(self) -> str: + return f"stainless-python-retry-{uuid.uuid4()}" + + +class _DefaultHttpxClient(httpx.Client): + def __init__(self, **kwargs: Any) -> None: + kwargs.setdefault("timeout", DEFAULT_TIMEOUT) + kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) + kwargs.setdefault("follow_redirects", True) + super().__init__(**kwargs) + + +if TYPE_CHECKING: + DefaultHttpxClient = httpx.Client + """An alias to `httpx.Client` that provides the same defaults that this SDK + uses internally. + + This is useful because overriding the `http_client` with your own instance of + `httpx.Client` will result in httpx's defaults being used, not ours. + """ +else: + DefaultHttpxClient = _DefaultHttpxClient + + +class SyncHttpxClientWrapper(DefaultHttpxClient): + def __del__(self) -> None: + 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 + ) -> FinalRequestOptions: + """Hook for mutating the given options""" + return options + + def _prepare_request( + self, + request: httpx.Request, # noqa: ARG002 + ) -> None: + """This method is used as a callback for mutating the `Request` object + after it has been constructed. + This is useful for cases where you want to add certain headers based off of + the request properties, e.g. `url`, `method` etc. + """ + return None + + @overload + def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + 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: + 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, + ) + + 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) + + 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 remaining_retries > 0: + return self._retry_request( + input_options, + cast_to, + retries_taken=retries_taken, + 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 remaining_retries > 0: + return self._retry_request( + input_options, + cast_to, + retries_taken=retries_taken, + 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 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() + + 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, + 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 + 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) + 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, + *, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + response: httpx.Response, + stream: bool, + stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, + retries_taken: int = 0, + ) -> ResponseT: + origin = get_origin(cast_to) or cast_to + + if inspect.isclass(origin) and issubclass(origin, BaseAPIResponse): + if not issubclass(origin, APIResponse): + raise TypeError(f"API Response types must subclass {APIResponse}; Received {origin}") + + response_cls = cast("type[BaseAPIResponse[Any]]", cast_to) + return cast( + ResponseT, + response_cls( + raw=response, + client=self, + cast_to=extract_response_type(response_cls), + stream=stream, + stream_cls=stream_cls, + options=options, + retries_taken=retries_taken, + ), + ) + + if cast_to == httpx.Response: + return cast(ResponseT, response) + + api_response = APIResponse( + raw=response, + client=self, + cast_to=cast("type[ResponseT]", cast_to), # pyright: ignore[reportUnnecessaryCast] + stream=stream, + stream_cls=stream_cls, + options=options, + retries_taken=retries_taken, + ) + if bool(response.request.headers.get(RAW_RESPONSE_HEADER)): + return cast(ResponseT, api_response) + + return api_response.parse() + + def _request_api_list( + self, + model: Type[object], + page: Type[SyncPageT], + options: FinalRequestOptions, + ) -> SyncPageT: + def _parser(resp: SyncPageT) -> SyncPageT: + resp._set_private_attributes( + client=self, + model=model, + options=options, + ) + return resp + + options.post_parser = _parser + + return self.request(page, options, stream=False) + + @overload + def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: Literal[False] = False, + ) -> ResponseT: ... + + @overload + def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: Literal[True], + stream_cls: type[_StreamT], + ) -> _StreamT: ... + + @overload + def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: bool, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: ... + + def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: bool = False, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: + opts = FinalRequestOptions.construct(method="get", url=path, **options) + # cast is required because mypy complains about returning Any even though + # it understands the type variables + return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) + + @overload + def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + files: RequestFiles | None = None, + stream: Literal[False] = False, + ) -> ResponseT: ... + + @overload + def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + files: RequestFiles | None = None, + stream: Literal[True], + stream_cls: type[_StreamT], + ) -> _StreamT: ... + + @overload + def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + files: RequestFiles | None = None, + stream: bool, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: ... + + def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + files: RequestFiles | None = None, + stream: bool = False, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: + opts = FinalRequestOptions.construct( + method="post", url=path, json_data=body, files=to_httpx_files(files), **options + ) + return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) + + def patch( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + return self.request(cast_to, opts) + + def put( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + opts = FinalRequestOptions.construct( + method="put", url=path, json_data=body, files=to_httpx_files(files), **options + ) + return self.request(cast_to, opts) + + def delete( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + return self.request(cast_to, opts) + + def get_api_list( + self, + path: str, + *, + model: Type[object], + page: Type[SyncPageT], + body: Body | None = None, + options: RequestOptions = {}, + method: str = "get", + ) -> SyncPageT: + opts = FinalRequestOptions.construct(method=method, url=path, json_data=body, **options) + return self._request_api_list(model, page, opts) + + +class _DefaultAsyncHttpxClient(httpx.AsyncClient): + def __init__(self, **kwargs: Any) -> None: + kwargs.setdefault("timeout", DEFAULT_TIMEOUT) + kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) + kwargs.setdefault("follow_redirects", True) + super().__init__(**kwargs) + + +if TYPE_CHECKING: + DefaultAsyncHttpxClient = httpx.AsyncClient + """An alias to `httpx.AsyncClient` that provides the same defaults that this SDK + uses internally. + + This is useful because overriding the `http_client` with your own instance of + `httpx.AsyncClient` will result in httpx's defaults being used, not ours. + """ +else: + DefaultAsyncHttpxClient = _DefaultAsyncHttpxClient + + +class AsyncHttpxClientWrapper(DefaultAsyncHttpxClient): + def __del__(self) -> None: + 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 + ) -> FinalRequestOptions: + """Hook for mutating the given options""" + return options + + async def _prepare_request( + self, + request: httpx.Request, # noqa: ARG002 + ) -> None: + """This method is used as a callback for mutating the `Request` object + after it has been constructed. + This is useful for cases where you want to add certain headers based off of + the request properties, e.g. `url`, `method` etc. + """ + return None + + @overload + async def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: Literal[False] = False, + 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: + 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)() + + # 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) + + 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 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("Raising timeout error") + raise APITimeoutError(request=request) from err + except Exception as err: + log.debug("Encountered Exception", exc_info=True) + + if retries_taken > 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("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 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() + + 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, + 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 + 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) + 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, + *, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + response: httpx.Response, + stream: bool, + stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, + retries_taken: int = 0, + ) -> ResponseT: + origin = get_origin(cast_to) or cast_to + + if inspect.isclass(origin) and issubclass(origin, BaseAPIResponse): + if not issubclass(origin, AsyncAPIResponse): + raise TypeError(f"API Response types must subclass {AsyncAPIResponse}; Received {origin}") + + response_cls = cast("type[BaseAPIResponse[Any]]", cast_to) + return cast( + "ResponseT", + response_cls( + raw=response, + client=self, + cast_to=extract_response_type(response_cls), + stream=stream, + stream_cls=stream_cls, + options=options, + retries_taken=retries_taken, + ), + ) + + if cast_to == httpx.Response: + return cast(ResponseT, response) + + api_response = AsyncAPIResponse( + raw=response, + client=self, + cast_to=cast("type[ResponseT]", cast_to), # pyright: ignore[reportUnnecessaryCast] + stream=stream, + stream_cls=stream_cls, + options=options, + retries_taken=retries_taken, + ) + if bool(response.request.headers.get(RAW_RESPONSE_HEADER)): + return cast(ResponseT, api_response) + + return await api_response.parse() + + def _request_api_list( + self, + model: Type[_T], + page: Type[AsyncPageT], + options: FinalRequestOptions, + ) -> AsyncPaginator[_T, AsyncPageT]: + return AsyncPaginator(client=self, options=options, page_cls=page, model=model) + + @overload + async def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: Literal[False] = False, + ) -> ResponseT: ... + + @overload + async def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: Literal[True], + stream_cls: type[_AsyncStreamT], + ) -> _AsyncStreamT: ... + + @overload + async def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: bool, + stream_cls: type[_AsyncStreamT] | None = None, + ) -> ResponseT | _AsyncStreamT: ... + + async def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: bool = False, + stream_cls: type[_AsyncStreamT] | None = None, + ) -> ResponseT | _AsyncStreamT: + opts = FinalRequestOptions.construct(method="get", url=path, **options) + return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) + + @overload + async def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + stream: Literal[False] = False, + ) -> ResponseT: ... + + @overload + async def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + stream: Literal[True], + stream_cls: type[_AsyncStreamT], + ) -> _AsyncStreamT: ... + + @overload + async def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + stream: bool, + stream_cls: type[_AsyncStreamT] | None = None, + ) -> ResponseT | _AsyncStreamT: ... + + async def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + stream: bool = False, + stream_cls: type[_AsyncStreamT] | None = None, + ) -> ResponseT | _AsyncStreamT: + opts = FinalRequestOptions.construct( + method="post", url=path, json_data=body, files=await async_to_httpx_files(files), **options + ) + return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) + + async def patch( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + return await self.request(cast_to, opts) + + async def put( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + opts = FinalRequestOptions.construct( + method="put", url=path, json_data=body, files=await async_to_httpx_files(files), **options + ) + return await self.request(cast_to, opts) + + async def delete( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + return await self.request(cast_to, opts) + + def get_api_list( + self, + path: str, + *, + model: Type[_T], + page: Type[AsyncPageT], + body: Body | None = None, + options: RequestOptions = {}, + method: str = "get", + ) -> AsyncPaginator[_T, AsyncPageT]: + opts = FinalRequestOptions.construct(method=method, url=path, json_data=body, **options) + return self._request_api_list(model, page, opts) + + +def make_request_options( + *, + query: Query | None = None, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + idempotency_key: str | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + post_parser: PostParser | NotGiven = NOT_GIVEN, +) -> RequestOptions: + """Create a dict of type RequestOptions without keys of NotGiven values.""" + options: RequestOptions = {} + if extra_headers is not None: + options["headers"] = extra_headers + + if extra_body is not None: + options["extra_json"] = cast(AnyMapping, extra_body) + + if query is not None: + options["params"] = query + + if extra_query is not None: + options["params"] = {**options.get("params", {}), **extra_query} + + if not isinstance(timeout, NotGiven): + options["timeout"] = timeout + + if idempotency_key is not None: + options["idempotency_key"] = idempotency_key + + if is_given(post_parser): + # internal + options["post_parser"] = post_parser # type: ignore + + return options + + +class ForceMultipartDict(Dict[str, None]): + def __bool__(self) -> bool: + return True + + +class OtherPlatform: + def __init__(self, name: str) -> None: + self.name = name + + @override + def __str__(self) -> str: + return f"Other:{self.name}" + + +Platform = Union[ + OtherPlatform, + Literal[ + "MacOS", + "Linux", + "Windows", + "FreeBSD", + "OpenBSD", + "iOS", + "Android", + "Unknown", + ], +] + + +def get_platform() -> Platform: + try: + system = platform.system().lower() + platform_name = platform.platform().lower() + except Exception: + return "Unknown" + + if "iphone" in platform_name or "ipad" in platform_name: + # Tested using Python3IDE on an iPhone 11 and Pythonista on an iPad 7 + # system is Darwin and platform_name is a string like: + # - Darwin-21.6.0-iPhone12,1-64bit + # - Darwin-21.6.0-iPad7,11-64bit + return "iOS" + + if system == "darwin": + return "MacOS" + + if system == "windows": + return "Windows" + + if "android" in platform_name: + # Tested using Pydroid 3 + # system is Linux and platform_name is a string like 'Linux-5.10.81-android12-9-00001-geba40aecb3b7-ab8534902-aarch64-with-libc' + return "Android" + + if system == "linux": + # https://distro.readthedocs.io/en/latest/#distro.id + distro_id = distro.id() + if distro_id == "freebsd": + return "FreeBSD" + + if distro_id == "openbsd": + return "OpenBSD" + + return "Linux" + + if platform_name: + return OtherPlatform(platform_name) + + return "Unknown" + + +@lru_cache(maxsize=None) +def platform_headers(version: str, *, platform: Platform | None) -> Dict[str, str]: + return { + "X-Stainless-Lang": "python", + "X-Stainless-Package-Version": version, + "X-Stainless-OS": str(platform or get_platform()), + "X-Stainless-Arch": str(get_architecture()), + "X-Stainless-Runtime": get_python_runtime(), + "X-Stainless-Runtime-Version": get_python_version(), + } + + +class OtherArch: + def __init__(self, name: str) -> None: + self.name = name + + @override + def __str__(self) -> str: + return f"other:{self.name}" + + +Arch = Union[OtherArch, Literal["x32", "x64", "arm", "arm64", "unknown"]] + + +def get_python_runtime() -> str: + try: + return platform.python_implementation() + except Exception: + return "unknown" + + +def get_python_version() -> str: + try: + return platform.python_version() + except Exception: + return "unknown" + + +def get_architecture() -> Arch: + try: + machine = platform.machine().lower() + except Exception: + return "unknown" + + if machine in ("arm64", "aarch64"): + return "arm64" + + # TODO: untested + if machine == "arm": + return "arm" + + if machine == "x86_64": + return "x64" + + # TODO: untested + if sys.maxsize <= 2**32: + return "x32" + + if machine: + return OtherArch(machine) + + return "unknown" + + +def _merge_mappings( + obj1: Mapping[_T_co, Union[_T, Omit]], + obj2: Mapping[_T_co, Union[_T, Omit]], +) -> Dict[_T_co, _T]: + """Merge two mappings of the same type, removing any values that are instances of `Omit`. + + In cases with duplicate keys the second mapping takes precedence. + """ + merged = {**obj1, **obj2} + return {key: value for key, value in merged.items() if not isinstance(value, Omit)} diff --git a/src/agility/_client.py b/src/agility/_client.py new file mode 100644 index 0000000..53abf1b --- /dev/null +++ b/src/agility/_client.py @@ -0,0 +1,532 @@ +# 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 AgilityError, APIStatusError +from ._base_client import ( + DEFAULT_MAX_RETRIES, + SyncAPIClient, + AsyncAPIClient, +) + +__all__ = [ + "Timeout", + "Transport", + "ProxiesTypes", + "RequestOptions", + "resources", + "Agility", + "AsyncAgility", + "Client", + "AsyncClient", +] + + +class Agility(SyncAPIClient): + assistants: resources.AssistantsResource + knowledge_bases: resources.KnowledgeBasesResource + health: resources.HealthResource + users: resources.UsersResource + threads: resources.ThreadsResource + with_raw_response: AgilityWithRawResponse + with_streaming_response: AgilityWithStreamedResponse + + # client options + bearer_token: str + api_key: str + access_key: str + + def __init__( + self, + *, + bearer_token: str | None = None, + api_key: str | None = None, + access_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 agility client instance. + + This automatically infers the following arguments from their corresponding environment variables if they are not provided: + - `bearer_token` from `BEARER_TOKEN` + - `api_key` from `AUTHENTICATED_API_KEY` + - `access_key` from `PUBLIC_ACCESS_KEY` + """ + if bearer_token is None: + bearer_token = os.environ.get("BEARER_TOKEN") + if bearer_token is None: + raise AgilityError( + "The bearer_token client option must be set either by passing bearer_token to the client or by setting the BEARER_TOKEN environment variable" + ) + self.bearer_token = bearer_token + + if api_key is None: + api_key = os.environ.get("AUTHENTICATED_API_KEY") + if api_key is None: + raise AgilityError( + "The api_key client option must be set either by passing api_key to the client or by setting the AUTHENTICATED_API_KEY environment variable" + ) + self.api_key = api_key + + if access_key is None: + access_key = os.environ.get("PUBLIC_ACCESS_KEY") + if access_key is None: + raise AgilityError( + "The access_key client option must be set either by passing access_key to the client or by setting the PUBLIC_ACCESS_KEY environment variable" + ) + self.access_key = access_key + + if base_url is None: + base_url = os.environ.get("AGILITY_BASE_URL") + if base_url is None: + base_url = f"https://localhost:8080/test-api" + + 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.assistants = resources.AssistantsResource(self) + self.knowledge_bases = resources.KnowledgeBasesResource(self) + self.health = resources.HealthResource(self) + self.users = resources.UsersResource(self) + self.threads = resources.ThreadsResource(self) + self.with_raw_response = AgilityWithRawResponse(self) + self.with_streaming_response = AgilityWithStreamedResponse(self) + + @property + @override + def qs(self) -> Querystring: + return Querystring(array_format="comma") + + @property + @override + def auth_headers(self) -> dict[str, str]: + if self._http_bearer: + return self._http_bearer + if self._authenticated_api_key: + return self._authenticated_api_key + if self._public_access_key: + return self._public_access_key + return {} + + @property + def _http_bearer(self) -> dict[str, str]: + bearer_token = self.bearer_token + return {"Authorization": f"Bearer {bearer_token}"} + + @property + def _authenticated_api_key(self) -> dict[str, str]: + api_key = self.api_key + return {"X-API-Key": api_key} + + @property + def _public_access_key(self) -> dict[str, str]: + access_key = self.access_key + return {"X-Access-Key": access_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, + *, + bearer_token: str | None = None, + api_key: str | None = None, + access_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__( + bearer_token=bearer_token or self.bearer_token, + api_key=api_key or self.api_key, + access_key=access_key or self.access_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 AsyncAgility(AsyncAPIClient): + assistants: resources.AsyncAssistantsResource + knowledge_bases: resources.AsyncKnowledgeBasesResource + health: resources.AsyncHealthResource + users: resources.AsyncUsersResource + threads: resources.AsyncThreadsResource + with_raw_response: AsyncAgilityWithRawResponse + with_streaming_response: AsyncAgilityWithStreamedResponse + + # client options + bearer_token: str + api_key: str + access_key: str + + def __init__( + self, + *, + bearer_token: str | None = None, + api_key: str | None = None, + access_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 agility client instance. + + This automatically infers the following arguments from their corresponding environment variables if they are not provided: + - `bearer_token` from `BEARER_TOKEN` + - `api_key` from `AUTHENTICATED_API_KEY` + - `access_key` from `PUBLIC_ACCESS_KEY` + """ + if bearer_token is None: + bearer_token = os.environ.get("BEARER_TOKEN") + if bearer_token is None: + raise AgilityError( + "The bearer_token client option must be set either by passing bearer_token to the client or by setting the BEARER_TOKEN environment variable" + ) + self.bearer_token = bearer_token + + if api_key is None: + api_key = os.environ.get("AUTHENTICATED_API_KEY") + if api_key is None: + raise AgilityError( + "The api_key client option must be set either by passing api_key to the client or by setting the AUTHENTICATED_API_KEY environment variable" + ) + self.api_key = api_key + + if access_key is None: + access_key = os.environ.get("PUBLIC_ACCESS_KEY") + if access_key is None: + raise AgilityError( + "The access_key client option must be set either by passing access_key to the client or by setting the PUBLIC_ACCESS_KEY environment variable" + ) + self.access_key = access_key + + if base_url is None: + base_url = os.environ.get("AGILITY_BASE_URL") + if base_url is None: + base_url = f"https://localhost:8080/test-api" + + 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.assistants = resources.AsyncAssistantsResource(self) + self.knowledge_bases = resources.AsyncKnowledgeBasesResource(self) + self.health = resources.AsyncHealthResource(self) + self.users = resources.AsyncUsersResource(self) + self.threads = resources.AsyncThreadsResource(self) + self.with_raw_response = AsyncAgilityWithRawResponse(self) + self.with_streaming_response = AsyncAgilityWithStreamedResponse(self) + + @property + @override + def qs(self) -> Querystring: + return Querystring(array_format="comma") + + @property + @override + def auth_headers(self) -> dict[str, str]: + if self._http_bearer: + return self._http_bearer + if self._authenticated_api_key: + return self._authenticated_api_key + if self._public_access_key: + return self._public_access_key + return {} + + @property + def _http_bearer(self) -> dict[str, str]: + bearer_token = self.bearer_token + return {"Authorization": f"Bearer {bearer_token}"} + + @property + def _authenticated_api_key(self) -> dict[str, str]: + api_key = self.api_key + return {"X-API-Key": api_key} + + @property + def _public_access_key(self) -> dict[str, str]: + access_key = self.access_key + return {"X-Access-Key": access_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, + *, + bearer_token: str | None = None, + api_key: str | None = None, + access_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__( + bearer_token=bearer_token or self.bearer_token, + api_key=api_key or self.api_key, + access_key=access_key or self.access_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 AgilityWithRawResponse: + def __init__(self, client: Agility) -> None: + self.assistants = resources.AssistantsResourceWithRawResponse(client.assistants) + self.knowledge_bases = resources.KnowledgeBasesResourceWithRawResponse(client.knowledge_bases) + self.health = resources.HealthResourceWithRawResponse(client.health) + self.users = resources.UsersResourceWithRawResponse(client.users) + self.threads = resources.ThreadsResourceWithRawResponse(client.threads) + + +class AsyncAgilityWithRawResponse: + def __init__(self, client: AsyncAgility) -> None: + self.assistants = resources.AsyncAssistantsResourceWithRawResponse(client.assistants) + self.knowledge_bases = resources.AsyncKnowledgeBasesResourceWithRawResponse(client.knowledge_bases) + self.health = resources.AsyncHealthResourceWithRawResponse(client.health) + self.users = resources.AsyncUsersResourceWithRawResponse(client.users) + self.threads = resources.AsyncThreadsResourceWithRawResponse(client.threads) + + +class AgilityWithStreamedResponse: + def __init__(self, client: Agility) -> None: + self.assistants = resources.AssistantsResourceWithStreamingResponse(client.assistants) + self.knowledge_bases = resources.KnowledgeBasesResourceWithStreamingResponse(client.knowledge_bases) + self.health = resources.HealthResourceWithStreamingResponse(client.health) + self.users = resources.UsersResourceWithStreamingResponse(client.users) + self.threads = resources.ThreadsResourceWithStreamingResponse(client.threads) + + +class AsyncAgilityWithStreamedResponse: + def __init__(self, client: AsyncAgility) -> None: + self.assistants = resources.AsyncAssistantsResourceWithStreamingResponse(client.assistants) + self.knowledge_bases = resources.AsyncKnowledgeBasesResourceWithStreamingResponse(client.knowledge_bases) + self.health = resources.AsyncHealthResourceWithStreamingResponse(client.health) + self.users = resources.AsyncUsersResourceWithStreamingResponse(client.users) + self.threads = resources.AsyncThreadsResourceWithStreamingResponse(client.threads) + + +Client = Agility + +AsyncClient = AsyncAgility diff --git a/src/agility/_compat.py b/src/agility/_compat.py new file mode 100644 index 0000000..162a6fb --- /dev/null +++ b/src/agility/_compat.py @@ -0,0 +1,219 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload +from datetime import date, datetime +from typing_extensions import Self + +import pydantic +from pydantic.fields import FieldInfo + +from ._types import IncEx, StrBytesIntFloat + +_T = TypeVar("_T") +_ModelT = TypeVar("_ModelT", bound=pydantic.BaseModel) + +# --------------- Pydantic v2 compatibility --------------- + +# Pyright incorrectly reports some of our functions as overriding a method when they don't +# pyright: reportIncompatibleMethodOverride=false + +PYDANTIC_V2 = pydantic.VERSION.startswith("2.") + +# v1 re-exports +if TYPE_CHECKING: + + def parse_date(value: date | StrBytesIntFloat) -> date: # noqa: ARG001 + ... + + def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime: # noqa: ARG001 + ... + + def get_args(t: type[Any]) -> tuple[Any, ...]: # noqa: ARG001 + ... + + def is_union(tp: type[Any] | None) -> bool: # noqa: ARG001 + ... + + def get_origin(t: type[Any]) -> type[Any] | None: # noqa: ARG001 + ... + + def is_literal_type(type_: type[Any]) -> bool: # noqa: ARG001 + ... + + def is_typeddict(type_: type[Any]) -> bool: # noqa: ARG001 + ... + +else: + if PYDANTIC_V2: + from pydantic.v1.typing import ( + get_args as get_args, + is_union as is_union, + get_origin as get_origin, + is_typeddict as is_typeddict, + is_literal_type as is_literal_type, + ) + from pydantic.v1.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime + else: + from pydantic.typing import ( + get_args as get_args, + is_union as is_union, + get_origin as get_origin, + is_typeddict as is_typeddict, + is_literal_type as is_literal_type, + ) + from pydantic.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime + + +# refactored config +if TYPE_CHECKING: + from pydantic import ConfigDict as ConfigDict +else: + if PYDANTIC_V2: + from pydantic import ConfigDict + else: + # TODO: provide an error message here? + ConfigDict = None + + +# renamed methods / properties +def parse_obj(model: type[_ModelT], value: object) -> _ModelT: + if PYDANTIC_V2: + return model.model_validate(value) + else: + return cast(_ModelT, model.parse_obj(value)) # pyright: ignore[reportDeprecated, reportUnnecessaryCast] + + +def field_is_required(field: FieldInfo) -> bool: + if PYDANTIC_V2: + return field.is_required() + return field.required # type: ignore + + +def field_get_default(field: FieldInfo) -> Any: + value = field.get_default() + if PYDANTIC_V2: + from pydantic_core import PydanticUndefined + + if value == PydanticUndefined: + return None + return value + return value + + +def field_outer_type(field: FieldInfo) -> Any: + if PYDANTIC_V2: + return field.annotation + return field.outer_type_ # type: ignore + + +def get_model_config(model: type[pydantic.BaseModel]) -> Any: + if PYDANTIC_V2: + return model.model_config + return model.__config__ # type: ignore + + +def get_model_fields(model: type[pydantic.BaseModel]) -> dict[str, FieldInfo]: + if PYDANTIC_V2: + return model.model_fields + return model.__fields__ # type: ignore + + +def model_copy(model: _ModelT, *, deep: bool = False) -> _ModelT: + if PYDANTIC_V2: + return model.model_copy(deep=deep) + return model.copy(deep=deep) # type: ignore + + +def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str: + if PYDANTIC_V2: + return model.model_dump_json(indent=indent) + return model.json(indent=indent) # type: ignore + + +def model_dump( + model: pydantic.BaseModel, + *, + exclude: IncEx = None, + 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]", + model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast] + exclude=exclude, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + ), + ) + + +def model_parse(model: type[_ModelT], data: Any) -> _ModelT: + if PYDANTIC_V2: + return model.model_validate(data) + return model.parse_obj(data) # pyright: ignore[reportDeprecated] + + +# generic models +if TYPE_CHECKING: + + class GenericModel(pydantic.BaseModel): ... + +else: + if PYDANTIC_V2: + # there no longer needs to be a distinction in v2 but + # we still have to create our own subclass to avoid + # inconsistent MRO ordering errors + class GenericModel(pydantic.BaseModel): ... + + else: + import pydantic.generics + + class GenericModel(pydantic.generics.GenericModel, pydantic.BaseModel): ... + + +# cached properties +if TYPE_CHECKING: + cached_property = property + + # we define a separate type (copied from typeshed) + # that represents that `cached_property` is `set`able + # at runtime, which differs from `@property`. + # + # this is a separate type as editors likely special case + # `@property` and we don't want to cause issues just to have + # more helpful internal types. + + class typed_cached_property(Generic[_T]): + func: Callable[[Any], _T] + attrname: str | None + + def __init__(self, func: Callable[[Any], _T]) -> None: ... + + @overload + def __get__(self, instance: None, owner: type[Any] | None = None) -> Self: ... + + @overload + def __get__(self, instance: object, owner: type[Any] | None = None) -> _T: ... + + def __get__(self, instance: object, owner: type[Any] | None = None) -> _T | Self: + raise NotImplementedError() + + def __set_name__(self, owner: type[Any], name: str) -> None: ... + + # __set__ is not defined at runtime, but @cached_property is designed to be settable + def __set__(self, instance: object, value: _T) -> None: ... +else: + 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/agility/_constants.py b/src/agility/_constants.py new file mode 100644 index 0000000..a2ac3b6 --- /dev/null +++ b/src/agility/_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/agility/_exceptions.py b/src/agility/_exceptions.py new file mode 100644 index 0000000..8f218f7 --- /dev/null +++ b/src/agility/_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 AgilityError(Exception): + pass + + +class APIError(AgilityError): + 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/agility/_files.py b/src/agility/_files.py new file mode 100644 index 0000000..715cc20 --- /dev/null +++ b/src/agility/_files.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +import io +import os +import pathlib +from typing import overload +from typing_extensions import TypeGuard + +import anyio + +from ._types import ( + FileTypes, + FileContent, + RequestFiles, + HttpxFileTypes, + Base64FileInput, + HttpxFileContent, + HttpxRequestFiles, +) +from ._utils import is_tuple_t, is_mapping_t, is_sequence_t + + +def is_base64_file_input(obj: object) -> TypeGuard[Base64FileInput]: + return isinstance(obj, io.IOBase) or isinstance(obj, os.PathLike) + + +def is_file_content(obj: object) -> TypeGuard[FileContent]: + return ( + isinstance(obj, bytes) or isinstance(obj, tuple) or isinstance(obj, io.IOBase) or isinstance(obj, os.PathLike) + ) + + +def assert_is_file_content(obj: object, *, key: str | None = None) -> None: + if not is_file_content(obj): + prefix = f"Expected entry at `{key}`" if key is not None else f"Expected file input `{obj!r}`" + raise RuntimeError( + f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead." + ) 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/agility/_models.py b/src/agility/_models.py new file mode 100644 index 0000000..d386eaa --- /dev/null +++ b/src/agility/_models.py @@ -0,0 +1,785 @@ +from __future__ import annotations + +import os +import inspect +from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, cast +from datetime import date, datetime +from typing_extensions import ( + Unpack, + Literal, + ClassVar, + Protocol, + Required, + ParamSpec, + TypedDict, + TypeGuard, + final, + override, + runtime_checkable, +) + +import pydantic +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, LiteralSchema, ModelFieldsSchema + +__all__ = ["BaseModel", "GenericModel"] + +_T = TypeVar("_T") +_BaseModelT = TypeVar("_BaseModelT", bound="BaseModel") + +P = ParamSpec("P") + + +@runtime_checkable +class _ConfigProtocol(Protocol): + allow_population_by_field_name: bool + + +class BaseModel(pydantic.BaseModel): + if PYDANTIC_V2: + model_config: ClassVar[ConfigDict] = ConfigDict( + extra="allow", defer_build=coerce_boolean(os.environ.get("DEFER_PYDANTIC_BUILD", "true")) + ) + else: + + @property + @override + def model_fields_set(self) -> set[str]: + # a forwards-compat shim for pydantic v2 + return self.__fields_set__ # type: ignore + + class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated] + extra: Any = pydantic.Extra.allow # type: ignore + + def to_dict( + self, + *, + mode: Literal["json", "python"] = "python", + use_api_names: bool = True, + exclude_unset: bool = True, + exclude_defaults: bool = False, + exclude_none: bool = False, + warnings: bool = True, + ) -> dict[str, object]: + """Recursively generate a dictionary representation of the model, optionally specifying which fields to include or exclude. + + By default, fields that were not set by the API will not be included, + and keys will match the API response, *not* the property names from the model. + + For example, if the API responds with `"fooBar": true` but we've defined a `foo_bar: bool` property, + the output will use the `"fooBar"` key (unless `use_api_names=False` is passed). + + Args: + mode: + If mode is 'json', the dictionary will only contain JSON serializable types. e.g. `datetime` will be turned into a string, `"2024-3-22T18:11:19.117000Z"`. + If mode is 'python', the dictionary may contain any Python objects. e.g. `datetime(2024, 3, 22)` + + use_api_names: Whether to use the key that the API responded with or the property name. Defaults to `True`. + exclude_unset: Whether to exclude fields that have not been explicitly set. + exclude_defaults: Whether to exclude fields that are set to their default value from the output. + exclude_none: Whether to exclude fields that have a value of `None` from the output. + warnings: Whether to log warnings when invalid fields are encountered. This is only supported in Pydantic v2. + """ + return self.model_dump( + mode=mode, + by_alias=use_api_names, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + warnings=warnings, + ) + + def to_json( + self, + *, + indent: int | None = 2, + use_api_names: bool = True, + exclude_unset: bool = True, + exclude_defaults: bool = False, + exclude_none: bool = False, + warnings: bool = True, + ) -> str: + """Generates a JSON string representing this model as it would be received from or sent to the API (but with indentation). + + By default, fields that were not set by the API will not be included, + and keys will match the API response, *not* the property names from the model. + + For example, if the API responds with `"fooBar": true` but we've defined a `foo_bar: bool` property, + the output will use the `"fooBar"` key (unless `use_api_names=False` is passed). + + Args: + indent: Indentation to use in the JSON output. If `None` is passed, the output will be compact. Defaults to `2` + use_api_names: Whether to use the key that the API responded with or the property name. Defaults to `True`. + exclude_unset: Whether to exclude fields that have not been explicitly set. + exclude_defaults: Whether to exclude fields that have the default value. + exclude_none: Whether to exclude fields that have a value of `None`. + warnings: Whether to show any warnings that occurred during serialization. This is only supported in Pydantic v2. + """ + return self.model_dump_json( + indent=indent, + by_alias=use_api_names, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + warnings=warnings, + ) + + @override + def __str__(self) -> str: + # mypy complains about an invalid self arg + return f'{self.__repr_name__()}({self.__repr_str__(", ")})' # type: ignore[misc] + + # Override the 'construct' method in a way that supports recursive parsing without validation. + # Based on https://github.com/samuelcolvin/pydantic/issues/1168#issuecomment-817742836. + @classmethod + @override + def construct( + 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 | Literal["none", "warn", "error"] = True, + context: dict[str, Any] | None = None, + serialize_as_any: bool = False, + ) -> dict[str, Any]: + """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump + + Generate a dictionary representation of the model, optionally specifying which fields to include or exclude. + + Args: + mode: The mode in which `to_python` should run. + If mode is 'json', the dictionary will only contain JSON serializable types. + If mode is 'python', the dictionary may contain any Python objects. + include: A list of fields to include in the output. + exclude: A list of fields to exclude from the output. + by_alias: Whether to use the field's alias in the dictionary key if defined. + exclude_unset: Whether to exclude fields that are unset or None from the output. + exclude_defaults: Whether to exclude fields that are set to their default value from the output. + exclude_none: Whether to exclude fields that have a value of `None` from the output. + round_trip: Whether to enable serialization and deserialization round-trip support. + warnings: Whether to log warnings when invalid fields are encountered. + + Returns: + A dictionary representation of the model. + """ + if mode != "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") + 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, + 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 | Literal["none", "warn", "error"] = True, + context: dict[str, Any] | None = None, + serialize_as_any: bool = False, + ) -> str: + """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump_json + + Generates a JSON representation of the model using Pydantic's `to_json` method. + + Args: + indent: Indentation to use in the JSON output. If None is passed, the output will be compact. + include: Field(s) to include in the JSON output. Can take either a string or set of strings. + exclude: Field(s) to exclude from the JSON output. Can take either a string or set of strings. + by_alias: Whether to serialize using field aliases. + exclude_unset: Whether to exclude fields that have not been explicitly set. + exclude_defaults: Whether to exclude fields that have the default value. + exclude_none: Whether to exclude fields that have a value of `None`. + round_trip: Whether to use serialization/deserialization between JSON and class instance. + warnings: Whether to show any warnings that occurred during serialization. + + Returns: + A JSON string representation of the model. + """ + if round_trip != False: + raise ValueError("round_trip is only supported in Pydantic v2") + if warnings != True: + raise ValueError("warnings is only supported in Pydantic v2") + if context is not None: + raise ValueError("context is only supported in Pydantic v2") + if serialize_as_any != False: + raise ValueError("serialize_as_any is only supported in Pydantic v2") + return super().json( # type: ignore[reportDeprecated] + indent=indent, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + + +def _construct_field(value: object, field: FieldInfo, key: str) -> object: + if value is None: + return field_get_default(field) + + if PYDANTIC_V2: + type_ = field.annotation + else: + type_ = cast(type, field.outer_type_) # type: ignore + + if type_ is None: + raise RuntimeError(f"Unexpected field type is None for {key}") + + return construct_type(value=value, type_=type_) + + +def is_basemodel(type_: type) -> bool: + """Returns whether or not the given type is either a `BaseModel` or a union of `BaseModel`""" + if is_union(type_): + for variant in get_args(type_): + if is_basemodel(variant): + return True + + return False + + return is_basemodel_type(type_) + + +def is_basemodel_type(type_: type) -> TypeGuard[type[BaseModel] | type[GenericModel]]: + origin = get_origin(type_) or type_ + if not inspect.isclass(origin): + return False + return issubclass(origin, BaseModel) or issubclass(origin, GenericModel) + + +def build( + base_model_cls: Callable[P, _BaseModelT], + *args: P.args, + **kwargs: P.kwargs, +) -> _BaseModelT: + """Construct a BaseModel class without validation. + + This is useful for cases where you need to instantiate a `BaseModel` + from an API response as this provides type-safe params which isn't supported + by helpers like `construct_type()`. + + ```py + build(MyModel, my_field_a="foo", my_field_b=123) + ``` + """ + if args: + raise TypeError( + "Received positional arguments which are not supported; Keyword arguments must be used instead", + ) + + return cast(_BaseModelT, construct_type(type_=base_model_cls, value=kwargs)) + + +def construct_type_unchecked(*, value: object, type_: type[_T]) -> _T: + """Loose coercion to the expected type with construction of nested values. + + Note: the returned value from this function is not guaranteed to match the + given type. + """ + return cast(_T, construct_type(value=value, type_=type_)) + + +def construct_type(*, value: object, type_: object) -> object: + """Loose coercion to the expected type with construction of nested values. + + If the given value does not match the expected type then it is returned as-is. + """ + # 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 cast("LiteralSchema", field_schema)["expected"]: + if isinstance(entry, str): + mapping[entry] = variant + else: + field_info = cast("dict[str, FieldInfo]", variant.__fields__).get(discriminator_field_name) # pyright: ignore[reportDeprecated, reportUnnecessaryCast] + if not field_info: + continue + + # Note: if one variant defines an alias then they all should + discriminator_alias = field_info.alias + + if 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)) + + +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: + 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/agility/_qs.py b/src/agility/_qs.py new file mode 100644 index 0000000..274320c --- /dev/null +++ b/src/agility/_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/agility/_resource.py b/src/agility/_resource.py new file mode 100644 index 0000000..9469435 --- /dev/null +++ b/src/agility/_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 Agility, AsyncAgility + + +class SyncAPIResource: + _client: Agility + + def __init__(self, client: Agility) -> 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: AsyncAgility + + def __init__(self, client: AsyncAgility) -> 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/agility/_response.py b/src/agility/_response.py new file mode 100644 index 0000000..3ac2a64 --- /dev/null +++ b/src/agility/_response.py @@ -0,0 +1,824 @@ +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 AgilityError, APIResponseValidationError + +if TYPE_CHECKING: + from ._models import FinalRequestOptions + from ._base_client import BaseClient + + +P = ParamSpec("P") +R = TypeVar("R") +_T = TypeVar("_T") +_APIResponseT = TypeVar("_APIResponseT", bound="APIResponse[Any]") +_AsyncAPIResponseT = TypeVar("_AsyncAPIResponseT", bound="AsyncAPIResponse[Any]") + +log: logging.Logger = logging.getLogger(__name__) + + +class BaseAPIResponse(Generic[R]): + _cast_to: type[R] + _client: BaseClient[Any, Any] + _parsed_by_type: dict[type[Any], Any] + _is_sse_stream: bool + _stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None + _options: FinalRequestOptions + + http_response: httpx.Response + + retries_taken: int + """The number of retries made. If no retries happened this will be `0`""" + + def __init__( + self, + *, + raw: httpx.Response, + cast_to: type[R], + client: BaseClient[Any, Any], + stream: bool, + stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, + options: FinalRequestOptions, + retries_taken: int = 0, + ) -> None: + self._cast_to = cast_to + self._client = client + self._parsed_by_type = {} + self._is_sse_stream = stream + self._stream_cls = stream_cls + self._options = options + self.http_response = raw + self.retries_taken = retries_taken + + @property + def headers(self) -> httpx.Headers: + return self.http_response.headers + + @property + def http_request(self) -> httpx.Request: + """Returns the httpx Request instance associated with the current response.""" + return self.http_response.request + + @property + def status_code(self) -> int: + return self.http_response.status_code + + @property + def url(self) -> httpx.URL: + """Returns the URL for which the request was made.""" + return self.http_response.url + + @property + def method(self) -> str: + return self.http_request.method + + @property + def http_version(self) -> str: + return self.http_response.http_version + + @property + def elapsed(self) -> datetime.timedelta: + """The time taken for the complete request/response cycle to complete.""" + return self.http_response.elapsed + + @property + def is_closed(self) -> bool: + """Whether or not the response body has been closed. + + If this is False then there is response data that has not been read yet. + You must either fully consume the response body or call `.close()` + before discarding the response to prevent resource leaks. + """ + return self.http_response.is_closed + + @override + def __repr__(self) -> str: + return ( + f"<{self.__class__.__name__} [{self.status_code} {self.http_response.reason_phrase}] type={self._cast_to}>" + ) + + def _parse(self, *, to: type[_T] | None = None) -> R | _T: + # 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)) + + 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`") + + 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 agility 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 agility 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 agility 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 `agility._streaming` for reference", + ) + + +class StreamAlreadyConsumed(AgilityError): + """ + 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/agility/_streaming.py b/src/agility/_streaming.py new file mode 100644 index 0000000..5c1fb13 --- /dev/null +++ b/src/agility/_streaming.py @@ -0,0 +1,333 @@ +# Note: initially copied from https://github.com/florimondmanca/httpx-sse/blob/master/src/httpx_sse/_decoders.py +from __future__ import annotations + +import json +import inspect +from types import TracebackType +from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, AsyncIterator, cast +from typing_extensions import Self, Protocol, TypeGuard, override, get_origin, runtime_checkable + +import httpx + +from ._utils import extract_type_var_from_base + +if TYPE_CHECKING: + from ._client import Agility, AsyncAgility + + +_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: Agility, + ) -> None: + self.response = response + self._cast_to = cast_to + self._client = client + self._decoder = client._make_sse_decoder() + self._iterator = self.__stream__() + + def __next__(self) -> _T: + return self._iterator.__next__() + + def __iter__(self) -> Iterator[_T]: + for item in self._iterator: + yield item + + def _iter_events(self) -> Iterator[ServerSentEvent]: + yield from self._decoder.iter_bytes(self.response.iter_bytes()) + + def __stream__(self) -> Iterator[_T]: + cast_to = cast(Any, self._cast_to) + response = self.response + process_data = self._client._process_response_data + iterator = self._iter_events() + + for sse in iterator: + yield process_data(data=sse.json(), cast_to=cast_to, response=response) + + # Ensure the entire stream is consumed + for _sse in iterator: + ... + + def __enter__(self) -> Self: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.close() + + def close(self) -> None: + """ + Close the response and release the connection. + + Automatically called if the response body is read to completion. + """ + self.response.close() + + +class AsyncStream(Generic[_T]): + """Provides the core interface to iterate over an asynchronous stream response.""" + + response: httpx.Response + + _decoder: SSEDecoder | SSEBytesDecoder + + def __init__( + self, + *, + cast_to: type[_T], + response: httpx.Response, + client: AsyncAgility, + ) -> None: + self.response = response + self._cast_to = cast_to + self._client = client + self._decoder = client._make_sse_decoder() + self._iterator = self.__stream__() + + async def __anext__(self) -> _T: + return await self._iterator.__anext__() + + async def __aiter__(self) -> AsyncIterator[_T]: + async for item in self._iterator: + yield item + + async def _iter_events(self) -> AsyncIterator[ServerSentEvent]: + async for sse in self._decoder.aiter_bytes(self.response.aiter_bytes()): + yield sse + + async def __stream__(self) -> AsyncIterator[_T]: + cast_to = cast(Any, self._cast_to) + response = self.response + process_data = self._client._process_response_data + iterator = self._iter_events() + + async for sse in iterator: + yield process_data(data=sse.json(), cast_to=cast_to, response=response) + + # Ensure the entire stream is consumed + async for _sse in iterator: + ... + + async def __aenter__(self) -> Self: + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + await self.close() + + async def close(self) -> None: + """ + Close the response and release the connection. + + Automatically called if the response body is read to completion. + """ + await self.response.aclose() + + +class ServerSentEvent: + def __init__( + self, + *, + event: str | None = None, + data: str | None = None, + id: str | None = None, + retry: int | None = None, + ) -> None: + if data is None: + data = "" + + self._id = id + self._data = data + self._event = event or None + self._retry = retry + + @property + def event(self) -> str | None: + return self._event + + @property + def id(self) -> str | None: + return self._id + + @property + def retry(self) -> int | None: + return self._retry + + @property + def data(self) -> str: + return self._data + + def json(self) -> Any: + return json.loads(self.data) + + @override + def __repr__(self) -> str: + return f"ServerSentEvent(event={self.event}, data={self.data}, id={self.id}, retry={self.retry})" + + +class SSEDecoder: + _data: list[str] + _event: str | None + _retry: int | None + _last_event_id: str | None + + def __init__(self) -> None: + self._event = None + self._data = [] + self._last_event_id = None + self._retry = None + + def iter_bytes(self, iterator: Iterator[bytes]) -> Iterator[ServerSentEvent]: + """Given an iterator that yields raw binary data, iterate over it & yield every event encountered""" + for chunk in self._iter_chunks(iterator): + # Split before decoding so splitlines() only uses \r and \n + for raw_line in chunk.splitlines(): + line = raw_line.decode("utf-8") + sse = self.decode(line) + if sse: + yield sse + + def _iter_chunks(self, iterator: Iterator[bytes]) -> Iterator[bytes]: + """Given an iterator that yields raw binary data, iterate over it and yield individual SSE chunks""" + data = b"" + for chunk in iterator: + for line in chunk.splitlines(keepends=True): + data += line + if data.endswith((b"\r\r", b"\n\n", b"\r\n\r\n")): + yield data + data = b"" + if data: + yield data + + async def aiter_bytes(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[ServerSentEvent]: + """Given an iterator that yields raw binary data, iterate over it & yield every event encountered""" + async for chunk in self._aiter_chunks(iterator): + # Split before decoding so splitlines() only uses \r and \n + for raw_line in chunk.splitlines(): + line = raw_line.decode("utf-8") + sse = self.decode(line) + if sse: + yield sse + + async def _aiter_chunks(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[bytes]: + """Given an iterator that yields raw binary data, iterate over it and yield individual SSE chunks""" + data = b"" + async for chunk in iterator: + for line in chunk.splitlines(keepends=True): + data += line + if data.endswith((b"\r\r", b"\n\n", b"\r\n\r\n")): + yield data + data = b"" + if data: + yield data + + def decode(self, line: str) -> ServerSentEvent | None: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = None + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None + + +@runtime_checkable +class SSEBytesDecoder(Protocol): + def iter_bytes(self, iterator: Iterator[bytes]) -> Iterator[ServerSentEvent]: + """Given an iterator that yields raw binary data, iterate over it & yield every event encountered""" + ... + + def aiter_bytes(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[ServerSentEvent]: + """Given an async iterator that yields raw binary data, iterate over it & yield every event encountered""" + ... + + +def is_stream_class_type(typ: type) -> TypeGuard[type[Stream[object]] | type[AsyncStream[object]]]: + """TypeGuard for determining whether or not the given type is a subclass of `Stream` / `AsyncStream`""" + origin = get_origin(typ) or typ + return inspect.isclass(origin) and issubclass(origin, (Stream, AsyncStream)) + + +def extract_stream_chunk_type( + stream_cls: type, + *, + failure_message: str | None = None, +) -> type: + """Given a type like `Stream[T]`, returns the generic type variable `T`. + + This also handles the case where a concrete subclass is given, e.g. + ```py + class MyStream(Stream[bytes]): + ... + + extract_stream_chunk_type(MyStream) -> bytes + ``` + """ + from ._base_client import Stream, AsyncStream + + return extract_type_var_from_base( + stream_cls, + index=0, + generic_bases=cast("tuple[type, ...]", (Stream, AsyncStream)), + failure_message=failure_message, + ) diff --git a/src/agility/_types.py b/src/agility/_types.py new file mode 100644 index 0000000..45f69c1 --- /dev/null +++ b/src/agility/_types.py @@ -0,0 +1,217 @@ +from __future__ import annotations + +from os import PathLike +from typing import ( + IO, + TYPE_CHECKING, + Any, + Dict, + List, + Type, + Tuple, + Union, + Mapping, + TypeVar, + Callable, + Optional, + Sequence, +) +from typing_extensions import 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 agility 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/agility/_utils/__init__.py b/src/agility/_utils/__init__.py new file mode 100644 index 0000000..3efe66c --- /dev/null +++ b/src/agility/_utils/__init__.py @@ -0,0 +1,55 @@ +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, +) +from ._reflection import ( + function_has_argument as function_has_argument, + assert_signatures_in_sync as assert_signatures_in_sync, +) diff --git a/src/agility/_utils/_logs.py b/src/agility/_utils/_logs.py new file mode 100644 index 0000000..0b6a292 --- /dev/null +++ b/src/agility/_utils/_logs.py @@ -0,0 +1,25 @@ +import os +import logging + +logger: logging.Logger = logging.getLogger("agility") +httpx_logger: logging.Logger = logging.getLogger("httpx") + + +def _basic_config() -> None: + # e.g. [2023-10-05 14:12:26 - agility._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("AGILITY_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/agility/_utils/_proxy.py b/src/agility/_utils/_proxy.py new file mode 100644 index 0000000..ffd883e --- /dev/null +++ b/src/agility/_utils/_proxy.py @@ -0,0 +1,62 @@ +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/agility/_utils/_reflection.py b/src/agility/_utils/_reflection.py new file mode 100644 index 0000000..89aa712 --- /dev/null +++ b/src/agility/_utils/_reflection.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import inspect +from typing import Any, Callable + + +def function_has_argument(func: Callable[..., Any], arg_name: str) -> bool: + """Returns whether or not the given function has a specific parameter""" + sig = inspect.signature(func) + return arg_name in sig.parameters + + +def assert_signatures_in_sync( + source_func: Callable[..., Any], + check_func: Callable[..., Any], + *, + exclude_params: set[str] = set(), +) -> None: + """Ensure that the signature of the second function matches the first.""" + + check_sig = inspect.signature(check_func) + source_sig = inspect.signature(source_func) + + errors: list[str] = [] + + for name, source_param in source_sig.parameters.items(): + if name in exclude_params: + continue + + custom_param = check_sig.parameters.get(name) + if not custom_param: + errors.append(f"the `{name}` param is missing") + continue + + if custom_param.annotation != source_param.annotation: + errors.append( + f"types for the `{name}` param are do not match; source={repr(source_param.annotation)} checking={repr(custom_param.annotation)}" + ) + continue + + if errors: + raise AssertionError(f"{len(errors)} errors encountered when comparing signatures:\n\n" + "\n\n".join(errors)) diff --git a/src/agility/_utils/_streams.py b/src/agility/_utils/_streams.py new file mode 100644 index 0000000..f4a0208 --- /dev/null +++ b/src/agility/_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/agility/_utils/_sync.py b/src/agility/_utils/_sync.py new file mode 100644 index 0000000..d0d8103 --- /dev/null +++ b/src/agility/_utils/_sync.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +import functools +from typing import 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]]: + """ + 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) + + # 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/agility/_utils/_transform.py b/src/agility/_utils/_transform.py new file mode 100644 index 0000000..47e262a --- /dev/null +++ b/src/agility/_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/agility/_utils/_typing.py b/src/agility/_utils/_typing.py new file mode 100644 index 0000000..c036991 --- /dev/null +++ b/src/agility/_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/agility/_utils/_utils.py b/src/agility/_utils/_utils.py new file mode 100644 index 0000000..0bba17c --- /dev/null +++ b/src/agility/_utils/_utils.py @@ -0,0 +1,397 @@ +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 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 is_mapping_t(headers): + # mypy doesn't understand the type narrowing here + for k, v in headers.items(): # type: ignore + if k.lower() == lower_header and isinstance(v, str): + return v + + # to deal with the case where the header looks like Stainless-Event-Id + intercaps_header = re.sub(r"([^\w])(\w)", lambda pat: pat.group(1) + pat.group(2).upper(), header.capitalize()) + + for normalized_header in [header, lower_header, header.upper(), intercaps_header]: + value = headers.get(normalized_header) + if value: + return value + + raise ValueError(f"Could not find {header} header") + + +def get_async_library() -> str: + try: + return sniffio.current_async_library() + except Exception: + return "false" + + +def lru_cache(*, maxsize: int | None = 128) -> Callable[[CallableT], CallableT]: + """A version of functools.lru_cache that retains the type signature + for the wrapped function arguments. + """ + wrapper = functools.lru_cache( # noqa: TID251 + maxsize=maxsize, + ) + return cast(Any, wrapper) # type: ignore[no-any-return] diff --git a/src/agility/_version.py b/src/agility/_version.py new file mode 100644 index 0000000..e384fa8 --- /dev/null +++ b/src/agility/_version.py @@ -0,0 +1,4 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +__title__ = "agility" +__version__ = "0.0.1-alpha.0" diff --git a/src/agility/lib/.keep b/src/agility/lib/.keep new file mode 100644 index 0000000..5e2c99f --- /dev/null +++ b/src/agility/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/agility/py.typed b/src/agility/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/agility/resources/__init__.py b/src/agility/resources/__init__.py new file mode 100644 index 0000000..02aa199 --- /dev/null +++ b/src/agility/resources/__init__.py @@ -0,0 +1,75 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .users import ( + UsersResource, + AsyncUsersResource, + UsersResourceWithRawResponse, + AsyncUsersResourceWithRawResponse, + UsersResourceWithStreamingResponse, + AsyncUsersResourceWithStreamingResponse, +) +from .health import ( + HealthResource, + AsyncHealthResource, + HealthResourceWithRawResponse, + AsyncHealthResourceWithRawResponse, + HealthResourceWithStreamingResponse, + AsyncHealthResourceWithStreamingResponse, +) +from .threads import ( + ThreadsResource, + AsyncThreadsResource, + ThreadsResourceWithRawResponse, + AsyncThreadsResourceWithRawResponse, + ThreadsResourceWithStreamingResponse, + AsyncThreadsResourceWithStreamingResponse, +) +from .assistants import ( + AssistantsResource, + AsyncAssistantsResource, + AssistantsResourceWithRawResponse, + AsyncAssistantsResourceWithRawResponse, + AssistantsResourceWithStreamingResponse, + AsyncAssistantsResourceWithStreamingResponse, +) +from .knowledge_bases import ( + KnowledgeBasesResource, + AsyncKnowledgeBasesResource, + KnowledgeBasesResourceWithRawResponse, + AsyncKnowledgeBasesResourceWithRawResponse, + KnowledgeBasesResourceWithStreamingResponse, + AsyncKnowledgeBasesResourceWithStreamingResponse, +) + +__all__ = [ + "AssistantsResource", + "AsyncAssistantsResource", + "AssistantsResourceWithRawResponse", + "AsyncAssistantsResourceWithRawResponse", + "AssistantsResourceWithStreamingResponse", + "AsyncAssistantsResourceWithStreamingResponse", + "KnowledgeBasesResource", + "AsyncKnowledgeBasesResource", + "KnowledgeBasesResourceWithRawResponse", + "AsyncKnowledgeBasesResourceWithRawResponse", + "KnowledgeBasesResourceWithStreamingResponse", + "AsyncKnowledgeBasesResourceWithStreamingResponse", + "HealthResource", + "AsyncHealthResource", + "HealthResourceWithRawResponse", + "AsyncHealthResourceWithRawResponse", + "HealthResourceWithStreamingResponse", + "AsyncHealthResourceWithStreamingResponse", + "UsersResource", + "AsyncUsersResource", + "UsersResourceWithRawResponse", + "AsyncUsersResourceWithRawResponse", + "UsersResourceWithStreamingResponse", + "AsyncUsersResourceWithStreamingResponse", + "ThreadsResource", + "AsyncThreadsResource", + "ThreadsResourceWithRawResponse", + "AsyncThreadsResourceWithRawResponse", + "ThreadsResourceWithStreamingResponse", + "AsyncThreadsResourceWithStreamingResponse", +] diff --git a/src/agility/resources/assistants/__init__.py b/src/agility/resources/assistants/__init__.py new file mode 100644 index 0000000..a5b7d56 --- /dev/null +++ b/src/agility/resources/assistants/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .assistants import ( + AssistantsResource, + AsyncAssistantsResource, + AssistantsResourceWithRawResponse, + AsyncAssistantsResourceWithRawResponse, + AssistantsResourceWithStreamingResponse, + AsyncAssistantsResourceWithStreamingResponse, +) +from .access_keys import ( + AccessKeysResource, + AsyncAccessKeysResource, + AccessKeysResourceWithRawResponse, + AsyncAccessKeysResourceWithRawResponse, + AccessKeysResourceWithStreamingResponse, + AsyncAccessKeysResourceWithStreamingResponse, +) + +__all__ = [ + "AccessKeysResource", + "AsyncAccessKeysResource", + "AccessKeysResourceWithRawResponse", + "AsyncAccessKeysResourceWithRawResponse", + "AccessKeysResourceWithStreamingResponse", + "AsyncAccessKeysResourceWithStreamingResponse", + "AssistantsResource", + "AsyncAssistantsResource", + "AssistantsResourceWithRawResponse", + "AsyncAssistantsResourceWithRawResponse", + "AssistantsResourceWithStreamingResponse", + "AsyncAssistantsResourceWithStreamingResponse", +] diff --git a/src/agility/resources/assistants/access_keys.py b/src/agility/resources/assistants/access_keys.py new file mode 100644 index 0000000..0537699 --- /dev/null +++ b/src/agility/resources/assistants/access_keys.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 Union, Optional +from datetime import datetime + +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.assistants import access_key_list_params, access_key_create_params +from ...types.assistants.access_key import AccessKey +from ...types.assistants.access_key_list_response import AccessKeyListResponse + +__all__ = ["AccessKeysResource", "AsyncAccessKeysResource"] + + +class AccessKeysResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> AccessKeysResourceWithRawResponse: + """ + 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers + """ + return AccessKeysResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AccessKeysResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/agility-python#with_streaming_response + """ + return AccessKeysResourceWithStreamingResponse(self) + + def create( + self, + assistant_id: str, + *, + name: str, + description: Optional[str] | NotGiven = NOT_GIVEN, + expires_at: Union[str, datetime, None] | 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, + ) -> AccessKey: + """ + Creates a new access_key. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not assistant_id: + raise ValueError(f"Expected a non-empty value for `assistant_id` but received {assistant_id!r}") + return self._post( + f"/api/assistants/{assistant_id}/access_keys/", + body=maybe_transform( + { + "name": name, + "description": description, + "expires_at": expires_at, + }, + access_key_create_params.AccessKeyCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AccessKey, + ) + + def retrieve( + self, + access_key_id: str, + *, + assistant_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AccessKey: + """ + Get a access_key by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not assistant_id: + raise ValueError(f"Expected a non-empty value for `assistant_id` but received {assistant_id!r}") + if not access_key_id: + raise ValueError(f"Expected a non-empty value for `access_key_id` but received {access_key_id!r}") + return self._get( + f"/api/assistants/{assistant_id}/access_keys/{access_key_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AccessKey, + ) + + def list( + self, + assistant_id: str, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AccessKeyListResponse: + """ + List all access keys for an assistant. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not assistant_id: + raise ValueError(f"Expected a non-empty value for `assistant_id` but received {assistant_id!r}") + return self._get( + f"/api/assistants/{assistant_id}/access_keys/", + 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, + }, + access_key_list_params.AccessKeyListParams, + ), + ), + cast_to=AccessKeyListResponse, + ) + + +class AsyncAccessKeysResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncAccessKeysResourceWithRawResponse: + """ + 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers + """ + return AsyncAccessKeysResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncAccessKeysResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/agility-python#with_streaming_response + """ + return AsyncAccessKeysResourceWithStreamingResponse(self) + + async def create( + self, + assistant_id: str, + *, + name: str, + description: Optional[str] | NotGiven = NOT_GIVEN, + expires_at: Union[str, datetime, None] | 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, + ) -> AccessKey: + """ + Creates a new access_key. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not assistant_id: + raise ValueError(f"Expected a non-empty value for `assistant_id` but received {assistant_id!r}") + return await self._post( + f"/api/assistants/{assistant_id}/access_keys/", + body=await async_maybe_transform( + { + "name": name, + "description": description, + "expires_at": expires_at, + }, + access_key_create_params.AccessKeyCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AccessKey, + ) + + async def retrieve( + self, + access_key_id: str, + *, + assistant_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AccessKey: + """ + Get a access_key by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not assistant_id: + raise ValueError(f"Expected a non-empty value for `assistant_id` but received {assistant_id!r}") + if not access_key_id: + raise ValueError(f"Expected a non-empty value for `access_key_id` but received {access_key_id!r}") + return await self._get( + f"/api/assistants/{assistant_id}/access_keys/{access_key_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AccessKey, + ) + + async def list( + self, + assistant_id: str, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AccessKeyListResponse: + """ + List all access keys for an assistant. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not assistant_id: + raise ValueError(f"Expected a non-empty value for `assistant_id` but received {assistant_id!r}") + return await self._get( + f"/api/assistants/{assistant_id}/access_keys/", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "limit": limit, + "offset": offset, + }, + access_key_list_params.AccessKeyListParams, + ), + ), + cast_to=AccessKeyListResponse, + ) + + +class AccessKeysResourceWithRawResponse: + def __init__(self, access_keys: AccessKeysResource) -> None: + self._access_keys = access_keys + + self.create = to_raw_response_wrapper( + access_keys.create, + ) + self.retrieve = to_raw_response_wrapper( + access_keys.retrieve, + ) + self.list = to_raw_response_wrapper( + access_keys.list, + ) + + +class AsyncAccessKeysResourceWithRawResponse: + def __init__(self, access_keys: AsyncAccessKeysResource) -> None: + self._access_keys = access_keys + + self.create = async_to_raw_response_wrapper( + access_keys.create, + ) + self.retrieve = async_to_raw_response_wrapper( + access_keys.retrieve, + ) + self.list = async_to_raw_response_wrapper( + access_keys.list, + ) + + +class AccessKeysResourceWithStreamingResponse: + def __init__(self, access_keys: AccessKeysResource) -> None: + self._access_keys = access_keys + + self.create = to_streamed_response_wrapper( + access_keys.create, + ) + self.retrieve = to_streamed_response_wrapper( + access_keys.retrieve, + ) + self.list = to_streamed_response_wrapper( + access_keys.list, + ) + + +class AsyncAccessKeysResourceWithStreamingResponse: + def __init__(self, access_keys: AsyncAccessKeysResource) -> None: + self._access_keys = access_keys + + self.create = async_to_streamed_response_wrapper( + access_keys.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + access_keys.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + access_keys.list, + ) diff --git a/src/agility/resources/assistants/assistants.py b/src/agility/resources/assistants/assistants.py new file mode 100644 index 0000000..111bf93 --- /dev/null +++ b/src/agility/resources/assistants/assistants.py @@ -0,0 +1,595 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import Literal + +import httpx + +from ...types import assistant_list_params, assistant_create_params, assistant_update_params +from ..._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven +from ..._utils import ( + maybe_transform, + async_maybe_transform, +) +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .access_keys import ( + AccessKeysResource, + AsyncAccessKeysResource, + AccessKeysResourceWithRawResponse, + AsyncAccessKeysResourceWithRawResponse, + AccessKeysResourceWithStreamingResponse, + AsyncAccessKeysResourceWithStreamingResponse, +) +from ..._base_client import make_request_options +from ...types.assistant import Assistant +from ...types.assistant_with_config import AssistantWithConfig +from ...types.assistant_list_response import AssistantListResponse + +__all__ = ["AssistantsResource", "AsyncAssistantsResource"] + + +class AssistantsResource(SyncAPIResource): + @cached_property + def access_keys(self) -> AccessKeysResource: + return AccessKeysResource(self._client) + + @cached_property + def with_raw_response(self) -> AssistantsResourceWithRawResponse: + """ + 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers + """ + return AssistantsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AssistantsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/agility-python#with_streaming_response + """ + return AssistantsResourceWithStreamingResponse(self) + + def create( + self, + *, + description: str, + knowledge_base_id: str, + name: str, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + model: Optional[Literal["gpt-4o"]] | 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, + ) -> Assistant: + """ + Create a new assistant. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters 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( + "/api/assistants/", + body=maybe_transform( + { + "description": description, + "knowledge_base_id": knowledge_base_id, + "name": name, + "instructions": instructions, + "model": model, + }, + assistant_create_params.AssistantCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Assistant, + ) + + def retrieve( + self, + assistant_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AssistantWithConfig: + """ + Get a single assistant by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not assistant_id: + raise ValueError(f"Expected a non-empty value for `assistant_id` but received {assistant_id!r}") + return self._get( + f"/api/assistants/{assistant_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AssistantWithConfig, + ) + + def update( + self, + assistant_id: str, + *, + id: str, + description: str, + knowledge_base_id: str, + name: str, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + model: Optional[Literal["gpt-4o"]] | 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, + ) -> AssistantWithConfig: + """ + Update an assistant. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not assistant_id: + raise ValueError(f"Expected a non-empty value for `assistant_id` but received {assistant_id!r}") + return self._put( + f"/api/assistants/{assistant_id}", + body=maybe_transform( + { + "id": id, + "description": description, + "knowledge_base_id": knowledge_base_id, + "name": name, + "instructions": instructions, + "model": model, + }, + assistant_update_params.AssistantUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AssistantWithConfig, + ) + + def list( + self, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AssistantListResponse: + """ + Get all assistants for the current user. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters 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/assistants/", + 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, + }, + assistant_list_params.AssistantListParams, + ), + ), + cast_to=AssistantListResponse, + ) + + def delete( + self, + assistant_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """ + Delete an assistant. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not assistant_id: + raise ValueError(f"Expected a non-empty value for `assistant_id` but received {assistant_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + f"/api/assistants/{assistant_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class AsyncAssistantsResource(AsyncAPIResource): + @cached_property + def access_keys(self) -> AsyncAccessKeysResource: + return AsyncAccessKeysResource(self._client) + + @cached_property + def with_raw_response(self) -> AsyncAssistantsResourceWithRawResponse: + """ + 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers + """ + return AsyncAssistantsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncAssistantsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/agility-python#with_streaming_response + """ + return AsyncAssistantsResourceWithStreamingResponse(self) + + async def create( + self, + *, + description: str, + knowledge_base_id: str, + name: str, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + model: Optional[Literal["gpt-4o"]] | 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, + ) -> Assistant: + """ + Create a new assistant. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters 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( + "/api/assistants/", + body=await async_maybe_transform( + { + "description": description, + "knowledge_base_id": knowledge_base_id, + "name": name, + "instructions": instructions, + "model": model, + }, + assistant_create_params.AssistantCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Assistant, + ) + + async def retrieve( + self, + assistant_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AssistantWithConfig: + """ + Get a single assistant by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not assistant_id: + raise ValueError(f"Expected a non-empty value for `assistant_id` but received {assistant_id!r}") + return await self._get( + f"/api/assistants/{assistant_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AssistantWithConfig, + ) + + async def update( + self, + assistant_id: str, + *, + id: str, + description: str, + knowledge_base_id: str, + name: str, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + model: Optional[Literal["gpt-4o"]] | 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, + ) -> AssistantWithConfig: + """ + Update an assistant. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not assistant_id: + raise ValueError(f"Expected a non-empty value for `assistant_id` but received {assistant_id!r}") + return await self._put( + f"/api/assistants/{assistant_id}", + body=await async_maybe_transform( + { + "id": id, + "description": description, + "knowledge_base_id": knowledge_base_id, + "name": name, + "instructions": instructions, + "model": model, + }, + assistant_update_params.AssistantUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AssistantWithConfig, + ) + + async def list( + self, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AssistantListResponse: + """ + Get all assistants for the current user. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._get( + "/api/assistants/", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "limit": limit, + "offset": offset, + }, + assistant_list_params.AssistantListParams, + ), + ), + cast_to=AssistantListResponse, + ) + + async def delete( + self, + assistant_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """ + Delete an assistant. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not assistant_id: + raise ValueError(f"Expected a non-empty value for `assistant_id` but received {assistant_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + f"/api/assistants/{assistant_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class AssistantsResourceWithRawResponse: + def __init__(self, assistants: AssistantsResource) -> None: + self._assistants = assistants + + self.create = to_raw_response_wrapper( + assistants.create, + ) + self.retrieve = to_raw_response_wrapper( + assistants.retrieve, + ) + self.update = to_raw_response_wrapper( + assistants.update, + ) + self.list = to_raw_response_wrapper( + assistants.list, + ) + self.delete = to_raw_response_wrapper( + assistants.delete, + ) + + @cached_property + def access_keys(self) -> AccessKeysResourceWithRawResponse: + return AccessKeysResourceWithRawResponse(self._assistants.access_keys) + + +class AsyncAssistantsResourceWithRawResponse: + def __init__(self, assistants: AsyncAssistantsResource) -> None: + self._assistants = assistants + + self.create = async_to_raw_response_wrapper( + assistants.create, + ) + self.retrieve = async_to_raw_response_wrapper( + assistants.retrieve, + ) + self.update = async_to_raw_response_wrapper( + assistants.update, + ) + self.list = async_to_raw_response_wrapper( + assistants.list, + ) + self.delete = async_to_raw_response_wrapper( + assistants.delete, + ) + + @cached_property + def access_keys(self) -> AsyncAccessKeysResourceWithRawResponse: + return AsyncAccessKeysResourceWithRawResponse(self._assistants.access_keys) + + +class AssistantsResourceWithStreamingResponse: + def __init__(self, assistants: AssistantsResource) -> None: + self._assistants = assistants + + self.create = to_streamed_response_wrapper( + assistants.create, + ) + self.retrieve = to_streamed_response_wrapper( + assistants.retrieve, + ) + self.update = to_streamed_response_wrapper( + assistants.update, + ) + self.list = to_streamed_response_wrapper( + assistants.list, + ) + self.delete = to_streamed_response_wrapper( + assistants.delete, + ) + + @cached_property + def access_keys(self) -> AccessKeysResourceWithStreamingResponse: + return AccessKeysResourceWithStreamingResponse(self._assistants.access_keys) + + +class AsyncAssistantsResourceWithStreamingResponse: + def __init__(self, assistants: AsyncAssistantsResource) -> None: + self._assistants = assistants + + self.create = async_to_streamed_response_wrapper( + assistants.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + assistants.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + assistants.update, + ) + self.list = async_to_streamed_response_wrapper( + assistants.list, + ) + self.delete = async_to_streamed_response_wrapper( + assistants.delete, + ) + + @cached_property + def access_keys(self) -> AsyncAccessKeysResourceWithStreamingResponse: + return AsyncAccessKeysResourceWithStreamingResponse(self._assistants.access_keys) diff --git a/src/agility/resources/health.py b/src/agility/resources/health.py new file mode 100644 index 0000000..28b4065 --- /dev/null +++ b/src/agility/resources/health.py @@ -0,0 +1,135 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.health_check_response import HealthCheckResponse + +__all__ = ["HealthResource", "AsyncHealthResource"] + + +class HealthResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> HealthResourceWithRawResponse: + """ + 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers + """ + return HealthResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> HealthResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/agility-python#with_streaming_response + """ + return HealthResourceWithStreamingResponse(self) + + def check( + 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, + ) -> HealthCheckResponse: + """Check the health of the application.""" + return self._get( + "/api/health/", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=HealthCheckResponse, + ) + + +class AsyncHealthResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncHealthResourceWithRawResponse: + """ + 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers + """ + return AsyncHealthResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncHealthResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/agility-python#with_streaming_response + """ + return AsyncHealthResourceWithStreamingResponse(self) + + async def check( + 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, + ) -> HealthCheckResponse: + """Check the health of the application.""" + return await self._get( + "/api/health/", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=HealthCheckResponse, + ) + + +class HealthResourceWithRawResponse: + def __init__(self, health: HealthResource) -> None: + self._health = health + + self.check = to_raw_response_wrapper( + health.check, + ) + + +class AsyncHealthResourceWithRawResponse: + def __init__(self, health: AsyncHealthResource) -> None: + self._health = health + + self.check = async_to_raw_response_wrapper( + health.check, + ) + + +class HealthResourceWithStreamingResponse: + def __init__(self, health: HealthResource) -> None: + self._health = health + + self.check = to_streamed_response_wrapper( + health.check, + ) + + +class AsyncHealthResourceWithStreamingResponse: + def __init__(self, health: AsyncHealthResource) -> None: + self._health = health + + self.check = async_to_streamed_response_wrapper( + health.check, + ) diff --git a/src/agility/resources/knowledge_bases/__init__.py b/src/agility/resources/knowledge_bases/__init__.py new file mode 100644 index 0000000..5e4fc5f --- /dev/null +++ b/src/agility/resources/knowledge_bases/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .sources import ( + SourcesResource, + AsyncSourcesResource, + SourcesResourceWithRawResponse, + AsyncSourcesResourceWithRawResponse, + SourcesResourceWithStreamingResponse, + AsyncSourcesResourceWithStreamingResponse, +) +from .knowledge_bases import ( + KnowledgeBasesResource, + AsyncKnowledgeBasesResource, + KnowledgeBasesResourceWithRawResponse, + AsyncKnowledgeBasesResourceWithRawResponse, + KnowledgeBasesResourceWithStreamingResponse, + AsyncKnowledgeBasesResourceWithStreamingResponse, +) + +__all__ = [ + "SourcesResource", + "AsyncSourcesResource", + "SourcesResourceWithRawResponse", + "AsyncSourcesResourceWithRawResponse", + "SourcesResourceWithStreamingResponse", + "AsyncSourcesResourceWithStreamingResponse", + "KnowledgeBasesResource", + "AsyncKnowledgeBasesResource", + "KnowledgeBasesResourceWithRawResponse", + "AsyncKnowledgeBasesResourceWithRawResponse", + "KnowledgeBasesResourceWithStreamingResponse", + "AsyncKnowledgeBasesResourceWithStreamingResponse", +] diff --git a/src/agility/resources/knowledge_bases/knowledge_bases.py b/src/agility/resources/knowledge_bases/knowledge_bases.py new file mode 100644 index 0000000..15f51a2 --- /dev/null +++ b/src/agility/resources/knowledge_bases/knowledge_bases.py @@ -0,0 +1,592 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ...types import ( + knowledge_base_list_params, + knowledge_base_create_params, + knowledge_base_update_params, +) +from .sources import ( + SourcesResource, + AsyncSourcesResource, + SourcesResourceWithRawResponse, + AsyncSourcesResourceWithRawResponse, + SourcesResourceWithStreamingResponse, + AsyncSourcesResourceWithStreamingResponse, +) +from ..._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven +from ..._utils import ( + maybe_transform, + async_maybe_transform, +) +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..._base_client import make_request_options +from .sources.sources import SourcesResource, AsyncSourcesResource +from ...types.knowledge_base_with_config import KnowledgeBaseWithConfig +from ...types.knowledge_base_list_response import KnowledgeBaseListResponse + +__all__ = ["KnowledgeBasesResource", "AsyncKnowledgeBasesResource"] + + +class KnowledgeBasesResource(SyncAPIResource): + @cached_property + def sources(self) -> SourcesResource: + return SourcesResource(self._client) + + @cached_property + def with_raw_response(self) -> KnowledgeBasesResourceWithRawResponse: + """ + 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers + """ + return KnowledgeBasesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> KnowledgeBasesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/agility-python#with_streaming_response + """ + return KnowledgeBasesResourceWithStreamingResponse(self) + + def create( + self, + *, + description: str, + ingestion_pipeline_params: knowledge_base_create_params.IngestionPipelineParams, + name: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> KnowledgeBaseWithConfig: + """ + Create a new knowledge base. + + Args: + ingestion_pipeline_params: Knowledge base pipeline params. + + Parameters defined on the knowledge-base level for a pipeline. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters 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( + "/api/knowledge_bases/", + body=maybe_transform( + { + "description": description, + "ingestion_pipeline_params": ingestion_pipeline_params, + "name": name, + }, + knowledge_base_create_params.KnowledgeBaseCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=KnowledgeBaseWithConfig, + ) + + def retrieve( + self, + knowledge_base_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> KnowledgeBaseWithConfig: + """ + Get a knowledge base by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not knowledge_base_id: + raise ValueError(f"Expected a non-empty value for `knowledge_base_id` but received {knowledge_base_id!r}") + return self._get( + f"/api/knowledge_bases/{knowledge_base_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=KnowledgeBaseWithConfig, + ) + + def update( + self, + knowledge_base_id: str, + *, + description: str, + ingestion_pipeline_params: knowledge_base_update_params.IngestionPipelineParams, + name: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> KnowledgeBaseWithConfig: + """ + Update a knowledge base. + + Args: + ingestion_pipeline_params: Knowledge base pipeline params. + + Parameters defined on the knowledge-base level for a pipeline. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not knowledge_base_id: + raise ValueError(f"Expected a non-empty value for `knowledge_base_id` but received {knowledge_base_id!r}") + return self._put( + f"/api/knowledge_bases/{knowledge_base_id}", + body=maybe_transform( + { + "description": description, + "ingestion_pipeline_params": ingestion_pipeline_params, + "name": name, + }, + knowledge_base_update_params.KnowledgeBaseUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=KnowledgeBaseWithConfig, + ) + + def list( + self, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> KnowledgeBaseListResponse: + """ + List all knowledge bases. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters 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/knowledge_bases/", + 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, + }, + knowledge_base_list_params.KnowledgeBaseListParams, + ), + ), + cast_to=KnowledgeBaseListResponse, + ) + + def delete( + self, + knowledge_base_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """ + Delete a knowledge base. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not knowledge_base_id: + raise ValueError(f"Expected a non-empty value for `knowledge_base_id` but received {knowledge_base_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + f"/api/knowledge_bases/{knowledge_base_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class AsyncKnowledgeBasesResource(AsyncAPIResource): + @cached_property + def sources(self) -> AsyncSourcesResource: + return AsyncSourcesResource(self._client) + + @cached_property + def with_raw_response(self) -> AsyncKnowledgeBasesResourceWithRawResponse: + """ + 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers + """ + return AsyncKnowledgeBasesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncKnowledgeBasesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/agility-python#with_streaming_response + """ + return AsyncKnowledgeBasesResourceWithStreamingResponse(self) + + async def create( + self, + *, + description: str, + ingestion_pipeline_params: knowledge_base_create_params.IngestionPipelineParams, + name: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> KnowledgeBaseWithConfig: + """ + Create a new knowledge base. + + Args: + ingestion_pipeline_params: Knowledge base pipeline params. + + Parameters defined on the knowledge-base level for a pipeline. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters 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( + "/api/knowledge_bases/", + body=await async_maybe_transform( + { + "description": description, + "ingestion_pipeline_params": ingestion_pipeline_params, + "name": name, + }, + knowledge_base_create_params.KnowledgeBaseCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=KnowledgeBaseWithConfig, + ) + + async def retrieve( + self, + knowledge_base_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> KnowledgeBaseWithConfig: + """ + Get a knowledge base by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not knowledge_base_id: + raise ValueError(f"Expected a non-empty value for `knowledge_base_id` but received {knowledge_base_id!r}") + return await self._get( + f"/api/knowledge_bases/{knowledge_base_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=KnowledgeBaseWithConfig, + ) + + async def update( + self, + knowledge_base_id: str, + *, + description: str, + ingestion_pipeline_params: knowledge_base_update_params.IngestionPipelineParams, + name: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> KnowledgeBaseWithConfig: + """ + Update a knowledge base. + + Args: + ingestion_pipeline_params: Knowledge base pipeline params. + + Parameters defined on the knowledge-base level for a pipeline. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not knowledge_base_id: + raise ValueError(f"Expected a non-empty value for `knowledge_base_id` but received {knowledge_base_id!r}") + return await self._put( + f"/api/knowledge_bases/{knowledge_base_id}", + body=await async_maybe_transform( + { + "description": description, + "ingestion_pipeline_params": ingestion_pipeline_params, + "name": name, + }, + knowledge_base_update_params.KnowledgeBaseUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=KnowledgeBaseWithConfig, + ) + + async def list( + self, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> KnowledgeBaseListResponse: + """ + List all knowledge bases. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._get( + "/api/knowledge_bases/", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "limit": limit, + "offset": offset, + }, + knowledge_base_list_params.KnowledgeBaseListParams, + ), + ), + cast_to=KnowledgeBaseListResponse, + ) + + async def delete( + self, + knowledge_base_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """ + Delete a knowledge base. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not knowledge_base_id: + raise ValueError(f"Expected a non-empty value for `knowledge_base_id` but received {knowledge_base_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + f"/api/knowledge_bases/{knowledge_base_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class KnowledgeBasesResourceWithRawResponse: + def __init__(self, knowledge_bases: KnowledgeBasesResource) -> None: + self._knowledge_bases = knowledge_bases + + self.create = to_raw_response_wrapper( + knowledge_bases.create, + ) + self.retrieve = to_raw_response_wrapper( + knowledge_bases.retrieve, + ) + self.update = to_raw_response_wrapper( + knowledge_bases.update, + ) + self.list = to_raw_response_wrapper( + knowledge_bases.list, + ) + self.delete = to_raw_response_wrapper( + knowledge_bases.delete, + ) + + @cached_property + def sources(self) -> SourcesResourceWithRawResponse: + return SourcesResourceWithRawResponse(self._knowledge_bases.sources) + + +class AsyncKnowledgeBasesResourceWithRawResponse: + def __init__(self, knowledge_bases: AsyncKnowledgeBasesResource) -> None: + self._knowledge_bases = knowledge_bases + + self.create = async_to_raw_response_wrapper( + knowledge_bases.create, + ) + self.retrieve = async_to_raw_response_wrapper( + knowledge_bases.retrieve, + ) + self.update = async_to_raw_response_wrapper( + knowledge_bases.update, + ) + self.list = async_to_raw_response_wrapper( + knowledge_bases.list, + ) + self.delete = async_to_raw_response_wrapper( + knowledge_bases.delete, + ) + + @cached_property + def sources(self) -> AsyncSourcesResourceWithRawResponse: + return AsyncSourcesResourceWithRawResponse(self._knowledge_bases.sources) + + +class KnowledgeBasesResourceWithStreamingResponse: + def __init__(self, knowledge_bases: KnowledgeBasesResource) -> None: + self._knowledge_bases = knowledge_bases + + self.create = to_streamed_response_wrapper( + knowledge_bases.create, + ) + self.retrieve = to_streamed_response_wrapper( + knowledge_bases.retrieve, + ) + self.update = to_streamed_response_wrapper( + knowledge_bases.update, + ) + self.list = to_streamed_response_wrapper( + knowledge_bases.list, + ) + self.delete = to_streamed_response_wrapper( + knowledge_bases.delete, + ) + + @cached_property + def sources(self) -> SourcesResourceWithStreamingResponse: + return SourcesResourceWithStreamingResponse(self._knowledge_bases.sources) + + +class AsyncKnowledgeBasesResourceWithStreamingResponse: + def __init__(self, knowledge_bases: AsyncKnowledgeBasesResource) -> None: + self._knowledge_bases = knowledge_bases + + self.create = async_to_streamed_response_wrapper( + knowledge_bases.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + knowledge_bases.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + knowledge_bases.update, + ) + self.list = async_to_streamed_response_wrapper( + knowledge_bases.list, + ) + self.delete = async_to_streamed_response_wrapper( + knowledge_bases.delete, + ) + + @cached_property + def sources(self) -> AsyncSourcesResourceWithStreamingResponse: + return AsyncSourcesResourceWithStreamingResponse(self._knowledge_bases.sources) diff --git a/src/agility/resources/knowledge_bases/sources/__init__.py b/src/agility/resources/knowledge_bases/sources/__init__.py new file mode 100644 index 0000000..47a8996 --- /dev/null +++ b/src/agility/resources/knowledge_bases/sources/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .sources import ( + SourcesResource, + AsyncSourcesResource, + SourcesResourceWithRawResponse, + AsyncSourcesResourceWithRawResponse, + SourcesResourceWithStreamingResponse, + AsyncSourcesResourceWithStreamingResponse, +) +from .documents import ( + DocumentsResource, + AsyncDocumentsResource, + DocumentsResourceWithRawResponse, + AsyncDocumentsResourceWithRawResponse, + DocumentsResourceWithStreamingResponse, + AsyncDocumentsResourceWithStreamingResponse, +) + +__all__ = [ + "DocumentsResource", + "AsyncDocumentsResource", + "DocumentsResourceWithRawResponse", + "AsyncDocumentsResourceWithRawResponse", + "DocumentsResourceWithStreamingResponse", + "AsyncDocumentsResourceWithStreamingResponse", + "SourcesResource", + "AsyncSourcesResource", + "SourcesResourceWithRawResponse", + "AsyncSourcesResourceWithRawResponse", + "SourcesResourceWithStreamingResponse", + "AsyncSourcesResourceWithStreamingResponse", +] diff --git a/src/agility/resources/knowledge_bases/sources/documents.py b/src/agility/resources/knowledge_bases/sources/documents.py new file mode 100644 index 0000000..018eb66 --- /dev/null +++ b/src/agility/resources/knowledge_bases/sources/documents.py @@ -0,0 +1,289 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ...._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ...._utils import ( + maybe_transform, + async_maybe_transform, +) +from ...._compat import cached_property +from ...._resource import SyncAPIResource, AsyncAPIResource +from ...._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ...._base_client import make_request_options +from ....types.knowledge_bases.sources import document_list_params +from ....types.knowledge_bases.sources.document import Document +from ....types.knowledge_bases.sources.document_list_response import DocumentListResponse + +__all__ = ["DocumentsResource", "AsyncDocumentsResource"] + + +class DocumentsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> DocumentsResourceWithRawResponse: + """ + 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers + """ + return DocumentsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> DocumentsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/agility-python#with_streaming_response + """ + return DocumentsResourceWithStreamingResponse(self) + + def retrieve( + self, + document_id: str, + *, + knowledge_base_id: str, + source_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Document: + """ + Get a document by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not knowledge_base_id: + raise ValueError(f"Expected a non-empty value for `knowledge_base_id` but received {knowledge_base_id!r}") + if not source_id: + raise ValueError(f"Expected a non-empty value for `source_id` but received {source_id!r}") + if not document_id: + raise ValueError(f"Expected a non-empty value for `document_id` but received {document_id!r}") + return self._get( + f"/api/knowledge_bases/{knowledge_base_id}/sources/{source_id}/documents/{document_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Document, + ) + + def list( + self, + source_id: str, + *, + knowledge_base_id: str, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DocumentListResponse: + """ + List all documents for a knowledge base. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not knowledge_base_id: + raise ValueError(f"Expected a non-empty value for `knowledge_base_id` but received {knowledge_base_id!r}") + if not source_id: + raise ValueError(f"Expected a non-empty value for `source_id` but received {source_id!r}") + return self._get( + f"/api/knowledge_bases/{knowledge_base_id}/sources/{source_id}/documents/", + 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, + }, + document_list_params.DocumentListParams, + ), + ), + cast_to=DocumentListResponse, + ) + + +class AsyncDocumentsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncDocumentsResourceWithRawResponse: + """ + 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers + """ + return AsyncDocumentsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncDocumentsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/agility-python#with_streaming_response + """ + return AsyncDocumentsResourceWithStreamingResponse(self) + + async def retrieve( + self, + document_id: str, + *, + knowledge_base_id: str, + source_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Document: + """ + Get a document by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not knowledge_base_id: + raise ValueError(f"Expected a non-empty value for `knowledge_base_id` but received {knowledge_base_id!r}") + if not source_id: + raise ValueError(f"Expected a non-empty value for `source_id` but received {source_id!r}") + if not document_id: + raise ValueError(f"Expected a non-empty value for `document_id` but received {document_id!r}") + return await self._get( + f"/api/knowledge_bases/{knowledge_base_id}/sources/{source_id}/documents/{document_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Document, + ) + + async def list( + self, + source_id: str, + *, + knowledge_base_id: str, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DocumentListResponse: + """ + List all documents for a knowledge base. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not knowledge_base_id: + raise ValueError(f"Expected a non-empty value for `knowledge_base_id` but received {knowledge_base_id!r}") + if not source_id: + raise ValueError(f"Expected a non-empty value for `source_id` but received {source_id!r}") + return await self._get( + f"/api/knowledge_bases/{knowledge_base_id}/sources/{source_id}/documents/", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "limit": limit, + "offset": offset, + }, + document_list_params.DocumentListParams, + ), + ), + cast_to=DocumentListResponse, + ) + + +class DocumentsResourceWithRawResponse: + def __init__(self, documents: DocumentsResource) -> None: + self._documents = documents + + self.retrieve = to_raw_response_wrapper( + documents.retrieve, + ) + self.list = to_raw_response_wrapper( + documents.list, + ) + + +class AsyncDocumentsResourceWithRawResponse: + def __init__(self, documents: AsyncDocumentsResource) -> None: + self._documents = documents + + self.retrieve = async_to_raw_response_wrapper( + documents.retrieve, + ) + self.list = async_to_raw_response_wrapper( + documents.list, + ) + + +class DocumentsResourceWithStreamingResponse: + def __init__(self, documents: DocumentsResource) -> None: + self._documents = documents + + self.retrieve = to_streamed_response_wrapper( + documents.retrieve, + ) + self.list = to_streamed_response_wrapper( + documents.list, + ) + + +class AsyncDocumentsResourceWithStreamingResponse: + def __init__(self, documents: AsyncDocumentsResource) -> None: + self._documents = documents + + self.retrieve = async_to_streamed_response_wrapper( + documents.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + documents.list, + ) diff --git a/src/agility/resources/knowledge_bases/sources/sources.py b/src/agility/resources/knowledge_bases/sources/sources.py new file mode 100644 index 0000000..73d47f9 --- /dev/null +++ b/src/agility/resources/knowledge_bases/sources/sources.py @@ -0,0 +1,794 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ...._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven +from ...._utils import ( + maybe_transform, + async_maybe_transform, +) +from .documents import ( + DocumentsResource, + AsyncDocumentsResource, + DocumentsResourceWithRawResponse, + AsyncDocumentsResourceWithRawResponse, + DocumentsResourceWithStreamingResponse, + AsyncDocumentsResourceWithStreamingResponse, +) +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.knowledge_bases import source_list_params, source_create_params, source_update_params +from ....types.knowledge_bases.source import Source +from ....types.knowledge_bases.source_list_response import SourceListResponse +from ....types.knowledge_bases.source_status_response import SourceStatusResponse + +__all__ = ["SourcesResource", "AsyncSourcesResource"] + + +class SourcesResource(SyncAPIResource): + @cached_property + def documents(self) -> DocumentsResource: + return DocumentsResource(self._client) + + @cached_property + def with_raw_response(self) -> SourcesResourceWithRawResponse: + """ + 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers + """ + return SourcesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> SourcesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/agility-python#with_streaming_response + """ + return SourcesResourceWithStreamingResponse(self) + + def create( + self, + knowledge_base_id: str, + *, + description: str, + name: str, + source_params: source_create_params.SourceParams, + source_schedule: source_create_params.SourceSchedule, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Source: + """ + Create a new source for an assistant. + + Args: + source_params: Parameters for web v0 sources. + + source_schedule: Source schedule model. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not knowledge_base_id: + raise ValueError(f"Expected a non-empty value for `knowledge_base_id` but received {knowledge_base_id!r}") + return self._post( + f"/api/knowledge_bases/{knowledge_base_id}/sources/", + body=maybe_transform( + { + "description": description, + "name": name, + "source_params": source_params, + "source_schedule": source_schedule, + }, + source_create_params.SourceCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Source, + ) + + def retrieve( + self, + source_id: str, + *, + knowledge_base_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Source: + """ + Get a single source by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not knowledge_base_id: + raise ValueError(f"Expected a non-empty value for `knowledge_base_id` but received {knowledge_base_id!r}") + if not source_id: + raise ValueError(f"Expected a non-empty value for `source_id` but received {source_id!r}") + return self._get( + f"/api/knowledge_bases/{knowledge_base_id}/sources/{source_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Source, + ) + + def update( + self, + source_id: str, + *, + knowledge_base_id: str, + description: str, + name: str, + source_params: source_update_params.SourceParams, + source_schedule: source_update_params.SourceSchedule, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Source: + """ + Update a source by ID. + + Args: + source_params: Parameters for web v0 sources. + + source_schedule: Source schedule model. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not knowledge_base_id: + raise ValueError(f"Expected a non-empty value for `knowledge_base_id` but received {knowledge_base_id!r}") + if not source_id: + raise ValueError(f"Expected a non-empty value for `source_id` but received {source_id!r}") + return self._put( + f"/api/knowledge_bases/{knowledge_base_id}/sources/{source_id}", + body=maybe_transform( + { + "description": description, + "name": name, + "source_params": source_params, + "source_schedule": source_schedule, + }, + source_update_params.SourceUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Source, + ) + + def list( + self, + knowledge_base_id: str, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SourceListResponse: + """ + Get all sources for a knowledge base. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not knowledge_base_id: + raise ValueError(f"Expected a non-empty value for `knowledge_base_id` but received {knowledge_base_id!r}") + return self._get( + f"/api/knowledge_bases/{knowledge_base_id}/sources/", + 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, + }, + source_list_params.SourceListParams, + ), + ), + cast_to=SourceListResponse, + ) + + def delete( + self, + source_id: str, + *, + knowledge_base_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """ + Delete a source by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not knowledge_base_id: + raise ValueError(f"Expected a non-empty value for `knowledge_base_id` but received {knowledge_base_id!r}") + if not source_id: + raise ValueError(f"Expected a non-empty value for `source_id` but received {source_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + f"/api/knowledge_bases/{knowledge_base_id}/sources/{source_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + def status( + self, + source_id: str, + *, + knowledge_base_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SourceStatusResponse: + """ + Get the status of a source by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not knowledge_base_id: + raise ValueError(f"Expected a non-empty value for `knowledge_base_id` but received {knowledge_base_id!r}") + if not source_id: + raise ValueError(f"Expected a non-empty value for `source_id` but received {source_id!r}") + return self._get( + f"/api/knowledge_bases/{knowledge_base_id}/sources/{source_id}/status", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SourceStatusResponse, + ) + + def sync( + self, + source_id: str, + *, + knowledge_base_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this 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: + """ + Sync a source by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not knowledge_base_id: + raise ValueError(f"Expected a non-empty value for `knowledge_base_id` but received {knowledge_base_id!r}") + if not source_id: + raise ValueError(f"Expected a non-empty value for `source_id` but received {source_id!r}") + return self._post( + f"/api/knowledge_bases/{knowledge_base_id}/sources/{source_id}/sync", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=object, + ) + + +class AsyncSourcesResource(AsyncAPIResource): + @cached_property + def documents(self) -> AsyncDocumentsResource: + return AsyncDocumentsResource(self._client) + + @cached_property + def with_raw_response(self) -> AsyncSourcesResourceWithRawResponse: + """ + 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers + """ + return AsyncSourcesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncSourcesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/agility-python#with_streaming_response + """ + return AsyncSourcesResourceWithStreamingResponse(self) + + async def create( + self, + knowledge_base_id: str, + *, + description: str, + name: str, + source_params: source_create_params.SourceParams, + source_schedule: source_create_params.SourceSchedule, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Source: + """ + Create a new source for an assistant. + + Args: + source_params: Parameters for web v0 sources. + + source_schedule: Source schedule model. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not knowledge_base_id: + raise ValueError(f"Expected a non-empty value for `knowledge_base_id` but received {knowledge_base_id!r}") + return await self._post( + f"/api/knowledge_bases/{knowledge_base_id}/sources/", + body=await async_maybe_transform( + { + "description": description, + "name": name, + "source_params": source_params, + "source_schedule": source_schedule, + }, + source_create_params.SourceCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Source, + ) + + async def retrieve( + self, + source_id: str, + *, + knowledge_base_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Source: + """ + Get a single source by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not knowledge_base_id: + raise ValueError(f"Expected a non-empty value for `knowledge_base_id` but received {knowledge_base_id!r}") + if not source_id: + raise ValueError(f"Expected a non-empty value for `source_id` but received {source_id!r}") + return await self._get( + f"/api/knowledge_bases/{knowledge_base_id}/sources/{source_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Source, + ) + + async def update( + self, + source_id: str, + *, + knowledge_base_id: str, + description: str, + name: str, + source_params: source_update_params.SourceParams, + source_schedule: source_update_params.SourceSchedule, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Source: + """ + Update a source by ID. + + Args: + source_params: Parameters for web v0 sources. + + source_schedule: Source schedule model. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not knowledge_base_id: + raise ValueError(f"Expected a non-empty value for `knowledge_base_id` but received {knowledge_base_id!r}") + if not source_id: + raise ValueError(f"Expected a non-empty value for `source_id` but received {source_id!r}") + return await self._put( + f"/api/knowledge_bases/{knowledge_base_id}/sources/{source_id}", + body=await async_maybe_transform( + { + "description": description, + "name": name, + "source_params": source_params, + "source_schedule": source_schedule, + }, + source_update_params.SourceUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Source, + ) + + async def list( + self, + knowledge_base_id: str, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SourceListResponse: + """ + Get all sources for a knowledge base. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not knowledge_base_id: + raise ValueError(f"Expected a non-empty value for `knowledge_base_id` but received {knowledge_base_id!r}") + return await self._get( + f"/api/knowledge_bases/{knowledge_base_id}/sources/", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "limit": limit, + "offset": offset, + }, + source_list_params.SourceListParams, + ), + ), + cast_to=SourceListResponse, + ) + + async def delete( + self, + source_id: str, + *, + knowledge_base_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """ + Delete a source by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not knowledge_base_id: + raise ValueError(f"Expected a non-empty value for `knowledge_base_id` but received {knowledge_base_id!r}") + if not source_id: + raise ValueError(f"Expected a non-empty value for `source_id` but received {source_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + f"/api/knowledge_bases/{knowledge_base_id}/sources/{source_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + async def status( + self, + source_id: str, + *, + knowledge_base_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SourceStatusResponse: + """ + Get the status of a source by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not knowledge_base_id: + raise ValueError(f"Expected a non-empty value for `knowledge_base_id` but received {knowledge_base_id!r}") + if not source_id: + raise ValueError(f"Expected a non-empty value for `source_id` but received {source_id!r}") + return await self._get( + f"/api/knowledge_bases/{knowledge_base_id}/sources/{source_id}/status", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SourceStatusResponse, + ) + + async def sync( + self, + source_id: str, + *, + knowledge_base_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this 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: + """ + Sync a source by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not knowledge_base_id: + raise ValueError(f"Expected a non-empty value for `knowledge_base_id` but received {knowledge_base_id!r}") + if not source_id: + raise ValueError(f"Expected a non-empty value for `source_id` but received {source_id!r}") + return await self._post( + f"/api/knowledge_bases/{knowledge_base_id}/sources/{source_id}/sync", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=object, + ) + + +class SourcesResourceWithRawResponse: + def __init__(self, sources: SourcesResource) -> None: + self._sources = sources + + self.create = to_raw_response_wrapper( + sources.create, + ) + self.retrieve = to_raw_response_wrapper( + sources.retrieve, + ) + self.update = to_raw_response_wrapper( + sources.update, + ) + self.list = to_raw_response_wrapper( + sources.list, + ) + self.delete = to_raw_response_wrapper( + sources.delete, + ) + self.status = to_raw_response_wrapper( + sources.status, + ) + self.sync = to_raw_response_wrapper( + sources.sync, + ) + + @cached_property + def documents(self) -> DocumentsResourceWithRawResponse: + return DocumentsResourceWithRawResponse(self._sources.documents) + + +class AsyncSourcesResourceWithRawResponse: + def __init__(self, sources: AsyncSourcesResource) -> None: + self._sources = sources + + self.create = async_to_raw_response_wrapper( + sources.create, + ) + self.retrieve = async_to_raw_response_wrapper( + sources.retrieve, + ) + self.update = async_to_raw_response_wrapper( + sources.update, + ) + self.list = async_to_raw_response_wrapper( + sources.list, + ) + self.delete = async_to_raw_response_wrapper( + sources.delete, + ) + self.status = async_to_raw_response_wrapper( + sources.status, + ) + self.sync = async_to_raw_response_wrapper( + sources.sync, + ) + + @cached_property + def documents(self) -> AsyncDocumentsResourceWithRawResponse: + return AsyncDocumentsResourceWithRawResponse(self._sources.documents) + + +class SourcesResourceWithStreamingResponse: + def __init__(self, sources: SourcesResource) -> None: + self._sources = sources + + self.create = to_streamed_response_wrapper( + sources.create, + ) + self.retrieve = to_streamed_response_wrapper( + sources.retrieve, + ) + self.update = to_streamed_response_wrapper( + sources.update, + ) + self.list = to_streamed_response_wrapper( + sources.list, + ) + self.delete = to_streamed_response_wrapper( + sources.delete, + ) + self.status = to_streamed_response_wrapper( + sources.status, + ) + self.sync = to_streamed_response_wrapper( + sources.sync, + ) + + @cached_property + def documents(self) -> DocumentsResourceWithStreamingResponse: + return DocumentsResourceWithStreamingResponse(self._sources.documents) + + +class AsyncSourcesResourceWithStreamingResponse: + def __init__(self, sources: AsyncSourcesResource) -> None: + self._sources = sources + + self.create = async_to_streamed_response_wrapper( + sources.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + sources.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + sources.update, + ) + self.list = async_to_streamed_response_wrapper( + sources.list, + ) + self.delete = async_to_streamed_response_wrapper( + sources.delete, + ) + self.status = async_to_streamed_response_wrapper( + sources.status, + ) + self.sync = async_to_streamed_response_wrapper( + sources.sync, + ) + + @cached_property + def documents(self) -> AsyncDocumentsResourceWithStreamingResponse: + return AsyncDocumentsResourceWithStreamingResponse(self._sources.documents) diff --git a/src/agility/resources/threads/__init__.py b/src/agility/resources/threads/__init__.py new file mode 100644 index 0000000..d56b6d0 --- /dev/null +++ b/src/agility/resources/threads/__init__.py @@ -0,0 +1,47 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .runs import ( + RunsResource, + AsyncRunsResource, + RunsResourceWithRawResponse, + AsyncRunsResourceWithRawResponse, + RunsResourceWithStreamingResponse, + AsyncRunsResourceWithStreamingResponse, +) +from .threads import ( + ThreadsResource, + AsyncThreadsResource, + ThreadsResourceWithRawResponse, + AsyncThreadsResourceWithRawResponse, + ThreadsResourceWithStreamingResponse, + AsyncThreadsResourceWithStreamingResponse, +) +from .messages import ( + MessagesResource, + AsyncMessagesResource, + MessagesResourceWithRawResponse, + AsyncMessagesResourceWithRawResponse, + MessagesResourceWithStreamingResponse, + AsyncMessagesResourceWithStreamingResponse, +) + +__all__ = [ + "MessagesResource", + "AsyncMessagesResource", + "MessagesResourceWithRawResponse", + "AsyncMessagesResourceWithRawResponse", + "MessagesResourceWithStreamingResponse", + "AsyncMessagesResourceWithStreamingResponse", + "RunsResource", + "AsyncRunsResource", + "RunsResourceWithRawResponse", + "AsyncRunsResourceWithRawResponse", + "RunsResourceWithStreamingResponse", + "AsyncRunsResourceWithStreamingResponse", + "ThreadsResource", + "AsyncThreadsResource", + "ThreadsResourceWithRawResponse", + "AsyncThreadsResourceWithRawResponse", + "ThreadsResourceWithStreamingResponse", + "AsyncThreadsResourceWithStreamingResponse", +] diff --git a/src/agility/resources/threads/messages.py b/src/agility/resources/threads/messages.py new file mode 100644 index 0000000..1ebab9e --- /dev/null +++ b/src/agility/resources/threads/messages.py @@ -0,0 +1,466 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import Literal + +import httpx + +from ..._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven +from ..._utils import ( + maybe_transform, + async_maybe_transform, +) +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..._base_client import make_request_options +from ...types.threads import message_list_params, message_create_params +from ...types.threads.message import Message +from ...types.threads.message_list_response import MessageListResponse + +__all__ = ["MessagesResource", "AsyncMessagesResource"] + + +class MessagesResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> MessagesResourceWithRawResponse: + """ + 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers + """ + return MessagesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> MessagesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/agility-python#with_streaming_response + """ + return MessagesResourceWithStreamingResponse(self) + + def create( + self, + thread_id: str, + *, + content: str, + metadata: Optional[message_create_params.Metadata], + role: Literal["user", "assistant"], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Message: + """ + Creates a new message. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + return self._post( + f"/api/threads/{thread_id}/messages/", + body=maybe_transform( + { + "content": content, + "metadata": metadata, + "role": role, + }, + message_create_params.MessageCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Message, + ) + + def retrieve( + self, + message_id: str, + *, + thread_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Message: + """ + Get a message by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + if not message_id: + raise ValueError(f"Expected a non-empty value for `message_id` but received {message_id!r}") + return self._get( + f"/api/threads/{thread_id}/messages/{message_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Message, + ) + + def list( + self, + thread_id: str, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> MessageListResponse: + """ + Lists messages for a given thread. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + return self._get( + f"/api/threads/{thread_id}/messages/", + 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, + }, + message_list_params.MessageListParams, + ), + ), + cast_to=MessageListResponse, + ) + + def delete( + self, + message_id: str, + *, + thread_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """ + Deletes a message by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + if not message_id: + raise ValueError(f"Expected a non-empty value for `message_id` but received {message_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + f"/api/threads/{thread_id}/messages/{message_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class AsyncMessagesResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncMessagesResourceWithRawResponse: + """ + 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers + """ + return AsyncMessagesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncMessagesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/agility-python#with_streaming_response + """ + return AsyncMessagesResourceWithStreamingResponse(self) + + async def create( + self, + thread_id: str, + *, + content: str, + metadata: Optional[message_create_params.Metadata], + role: Literal["user", "assistant"], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Message: + """ + Creates a new message. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + return await self._post( + f"/api/threads/{thread_id}/messages/", + body=await async_maybe_transform( + { + "content": content, + "metadata": metadata, + "role": role, + }, + message_create_params.MessageCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Message, + ) + + async def retrieve( + self, + message_id: str, + *, + thread_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Message: + """ + Get a message by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + if not message_id: + raise ValueError(f"Expected a non-empty value for `message_id` but received {message_id!r}") + return await self._get( + f"/api/threads/{thread_id}/messages/{message_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Message, + ) + + async def list( + self, + thread_id: str, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> MessageListResponse: + """ + Lists messages for a given thread. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + return await self._get( + f"/api/threads/{thread_id}/messages/", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "limit": limit, + "offset": offset, + }, + message_list_params.MessageListParams, + ), + ), + cast_to=MessageListResponse, + ) + + async def delete( + self, + message_id: str, + *, + thread_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """ + Deletes a message by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + if not message_id: + raise ValueError(f"Expected a non-empty value for `message_id` but received {message_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + f"/api/threads/{thread_id}/messages/{message_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class MessagesResourceWithRawResponse: + def __init__(self, messages: MessagesResource) -> None: + self._messages = messages + + self.create = to_raw_response_wrapper( + messages.create, + ) + self.retrieve = to_raw_response_wrapper( + messages.retrieve, + ) + self.list = to_raw_response_wrapper( + messages.list, + ) + self.delete = to_raw_response_wrapper( + messages.delete, + ) + + +class AsyncMessagesResourceWithRawResponse: + def __init__(self, messages: AsyncMessagesResource) -> None: + self._messages = messages + + self.create = async_to_raw_response_wrapper( + messages.create, + ) + self.retrieve = async_to_raw_response_wrapper( + messages.retrieve, + ) + self.list = async_to_raw_response_wrapper( + messages.list, + ) + self.delete = async_to_raw_response_wrapper( + messages.delete, + ) + + +class MessagesResourceWithStreamingResponse: + def __init__(self, messages: MessagesResource) -> None: + self._messages = messages + + self.create = to_streamed_response_wrapper( + messages.create, + ) + self.retrieve = to_streamed_response_wrapper( + messages.retrieve, + ) + self.list = to_streamed_response_wrapper( + messages.list, + ) + self.delete = to_streamed_response_wrapper( + messages.delete, + ) + + +class AsyncMessagesResourceWithStreamingResponse: + def __init__(self, messages: AsyncMessagesResource) -> None: + self._messages = messages + + self.create = async_to_streamed_response_wrapper( + messages.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + messages.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + messages.list, + ) + self.delete = async_to_streamed_response_wrapper( + messages.delete, + ) diff --git a/src/agility/resources/threads/runs.py b/src/agility/resources/threads/runs.py new file mode 100644 index 0000000..f76a5f7 --- /dev/null +++ b/src/agility/resources/threads/runs.py @@ -0,0 +1,487 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Iterable, Optional +from typing_extensions import Literal + +import httpx + +from ..._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven +from ..._utils import ( + maybe_transform, + async_maybe_transform, +) +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..._base_client import make_request_options +from ...types.threads import run_create_params, run_stream_params +from ...types.threads.run import Run + +__all__ = ["RunsResource", "AsyncRunsResource"] + + +class RunsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> RunsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers + """ + return RunsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> RunsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/agility-python#with_streaming_response + """ + return RunsResourceWithStreamingResponse(self) + + def create( + self, + thread_id: str, + *, + assistant_id: str, + additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + additional_messages: Iterable[run_create_params.AdditionalMessage] | NotGiven = NOT_GIVEN, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + knowledge_base_id: Optional[str] | NotGiven = NOT_GIVEN, + model: Optional[Literal["gpt-4o"]] | 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, + ) -> Run: + """ + Creates a new run, starting it in the background. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + return self._post( + f"/api/threads/{thread_id}/runs/", + body=maybe_transform( + { + "assistant_id": assistant_id, + "additional_instructions": additional_instructions, + "additional_messages": additional_messages, + "instructions": instructions, + "knowledge_base_id": knowledge_base_id, + "model": model, + }, + run_create_params.RunCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Run, + ) + + def retrieve( + self, + run_id: str, + *, + thread_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Run: + """ + Get a run by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + if not run_id: + raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") + return self._get( + f"/api/threads/{thread_id}/runs/{run_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Run, + ) + + def delete( + self, + run_id: str, + *, + thread_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """ + Deletes a run by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + if not run_id: + raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + f"/api/threads/{thread_id}/runs/{run_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + def stream( + self, + thread_id: str, + *, + assistant_id: str, + additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + additional_messages: Iterable[run_stream_params.AdditionalMessage] | NotGiven = NOT_GIVEN, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + knowledge_base_id: Optional[str] | NotGiven = NOT_GIVEN, + model: Optional[Literal["gpt-4o"]] | 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, + ) -> object: + """ + Creates a new run and streams the results. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + return self._post( + f"/api/threads/{thread_id}/runs/stream", + body=maybe_transform( + { + "assistant_id": assistant_id, + "additional_instructions": additional_instructions, + "additional_messages": additional_messages, + "instructions": instructions, + "knowledge_base_id": knowledge_base_id, + "model": model, + }, + run_stream_params.RunStreamParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=object, + ) + + +class AsyncRunsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncRunsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers + """ + return AsyncRunsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncRunsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/agility-python#with_streaming_response + """ + return AsyncRunsResourceWithStreamingResponse(self) + + async def create( + self, + thread_id: str, + *, + assistant_id: str, + additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + additional_messages: Iterable[run_create_params.AdditionalMessage] | NotGiven = NOT_GIVEN, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + knowledge_base_id: Optional[str] | NotGiven = NOT_GIVEN, + model: Optional[Literal["gpt-4o"]] | 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, + ) -> Run: + """ + Creates a new run, starting it in the background. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + return await self._post( + f"/api/threads/{thread_id}/runs/", + body=await async_maybe_transform( + { + "assistant_id": assistant_id, + "additional_instructions": additional_instructions, + "additional_messages": additional_messages, + "instructions": instructions, + "knowledge_base_id": knowledge_base_id, + "model": model, + }, + run_create_params.RunCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Run, + ) + + async def retrieve( + self, + run_id: str, + *, + thread_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Run: + """ + Get a run by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + if not run_id: + raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") + return await self._get( + f"/api/threads/{thread_id}/runs/{run_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Run, + ) + + async def delete( + self, + run_id: str, + *, + thread_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """ + Deletes a run by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + if not run_id: + raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + f"/api/threads/{thread_id}/runs/{run_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + async def stream( + self, + thread_id: str, + *, + assistant_id: str, + additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, + additional_messages: Iterable[run_stream_params.AdditionalMessage] | NotGiven = NOT_GIVEN, + instructions: Optional[str] | NotGiven = NOT_GIVEN, + knowledge_base_id: Optional[str] | NotGiven = NOT_GIVEN, + model: Optional[Literal["gpt-4o"]] | 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, + ) -> object: + """ + Creates a new run and streams the results. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + return await self._post( + f"/api/threads/{thread_id}/runs/stream", + body=await async_maybe_transform( + { + "assistant_id": assistant_id, + "additional_instructions": additional_instructions, + "additional_messages": additional_messages, + "instructions": instructions, + "knowledge_base_id": knowledge_base_id, + "model": model, + }, + run_stream_params.RunStreamParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=object, + ) + + +class RunsResourceWithRawResponse: + def __init__(self, runs: RunsResource) -> None: + self._runs = runs + + self.create = to_raw_response_wrapper( + runs.create, + ) + self.retrieve = to_raw_response_wrapper( + runs.retrieve, + ) + self.delete = to_raw_response_wrapper( + runs.delete, + ) + self.stream = to_raw_response_wrapper( + runs.stream, + ) + + +class AsyncRunsResourceWithRawResponse: + def __init__(self, runs: AsyncRunsResource) -> None: + self._runs = runs + + self.create = async_to_raw_response_wrapper( + runs.create, + ) + self.retrieve = async_to_raw_response_wrapper( + runs.retrieve, + ) + self.delete = async_to_raw_response_wrapper( + runs.delete, + ) + self.stream = async_to_raw_response_wrapper( + runs.stream, + ) + + +class RunsResourceWithStreamingResponse: + def __init__(self, runs: RunsResource) -> None: + self._runs = runs + + self.create = to_streamed_response_wrapper( + runs.create, + ) + self.retrieve = to_streamed_response_wrapper( + runs.retrieve, + ) + self.delete = to_streamed_response_wrapper( + runs.delete, + ) + self.stream = to_streamed_response_wrapper( + runs.stream, + ) + + +class AsyncRunsResourceWithStreamingResponse: + def __init__(self, runs: AsyncRunsResource) -> None: + self._runs = runs + + self.create = async_to_streamed_response_wrapper( + runs.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + runs.retrieve, + ) + self.delete = async_to_streamed_response_wrapper( + runs.delete, + ) + self.stream = async_to_streamed_response_wrapper( + runs.stream, + ) diff --git a/src/agility/resources/threads/threads.py b/src/agility/resources/threads/threads.py new file mode 100644 index 0000000..d0c2165 --- /dev/null +++ b/src/agility/resources/threads/threads.py @@ -0,0 +1,459 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from .runs import ( + RunsResource, + AsyncRunsResource, + RunsResourceWithRawResponse, + AsyncRunsResourceWithRawResponse, + RunsResourceWithStreamingResponse, + AsyncRunsResourceWithStreamingResponse, +) +from ...types import thread_list_params +from ..._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven +from ..._utils import ( + maybe_transform, + async_maybe_transform, +) +from .messages import ( + MessagesResource, + AsyncMessagesResource, + MessagesResourceWithRawResponse, + AsyncMessagesResourceWithRawResponse, + MessagesResourceWithStreamingResponse, + AsyncMessagesResourceWithStreamingResponse, +) +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.thread import Thread +from ...types.thread_list_response import ThreadListResponse + +__all__ = ["ThreadsResource", "AsyncThreadsResource"] + + +class ThreadsResource(SyncAPIResource): + @cached_property + def messages(self) -> MessagesResource: + return MessagesResource(self._client) + + @cached_property + def runs(self) -> RunsResource: + return RunsResource(self._client) + + @cached_property + def with_raw_response(self) -> ThreadsResourceWithRawResponse: + """ + 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers + """ + return ThreadsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ThreadsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/agility-python#with_streaming_response + """ + return ThreadsResourceWithStreamingResponse(self) + + def create( + 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, + ) -> Thread: + """Creates a new thread.""" + return self._post( + "/api/threads/", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Thread, + ) + + def retrieve( + self, + thread_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Thread: + """ + Get a thread by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + return self._get( + f"/api/threads/{thread_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Thread, + ) + + def list( + self, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ThreadListResponse: + """ + List all threads for a user. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters 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/threads/", + 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, + }, + thread_list_params.ThreadListParams, + ), + ), + cast_to=ThreadListResponse, + ) + + def delete( + self, + thread_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """ + Delete a thread. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + f"/api/threads/{thread_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class AsyncThreadsResource(AsyncAPIResource): + @cached_property + def messages(self) -> AsyncMessagesResource: + return AsyncMessagesResource(self._client) + + @cached_property + def runs(self) -> AsyncRunsResource: + return AsyncRunsResource(self._client) + + @cached_property + def with_raw_response(self) -> AsyncThreadsResourceWithRawResponse: + """ + 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers + """ + return AsyncThreadsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncThreadsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/agility-python#with_streaming_response + """ + return AsyncThreadsResourceWithStreamingResponse(self) + + async def create( + 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, + ) -> Thread: + """Creates a new thread.""" + return await self._post( + "/api/threads/", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Thread, + ) + + async def retrieve( + self, + thread_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Thread: + """ + Get a thread by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + return await self._get( + f"/api/threads/{thread_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Thread, + ) + + async def list( + self, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ThreadListResponse: + """ + List all threads for a user. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._get( + "/api/threads/", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "limit": limit, + "offset": offset, + }, + thread_list_params.ThreadListParams, + ), + ), + cast_to=ThreadListResponse, + ) + + async def delete( + self, + thread_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """ + Delete a thread. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + f"/api/threads/{thread_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class ThreadsResourceWithRawResponse: + def __init__(self, threads: ThreadsResource) -> None: + self._threads = threads + + self.create = to_raw_response_wrapper( + threads.create, + ) + self.retrieve = to_raw_response_wrapper( + threads.retrieve, + ) + self.list = to_raw_response_wrapper( + threads.list, + ) + self.delete = to_raw_response_wrapper( + threads.delete, + ) + + @cached_property + def messages(self) -> MessagesResourceWithRawResponse: + return MessagesResourceWithRawResponse(self._threads.messages) + + @cached_property + def runs(self) -> RunsResourceWithRawResponse: + return RunsResourceWithRawResponse(self._threads.runs) + + +class AsyncThreadsResourceWithRawResponse: + def __init__(self, threads: AsyncThreadsResource) -> None: + self._threads = threads + + self.create = async_to_raw_response_wrapper( + threads.create, + ) + self.retrieve = async_to_raw_response_wrapper( + threads.retrieve, + ) + self.list = async_to_raw_response_wrapper( + threads.list, + ) + self.delete = async_to_raw_response_wrapper( + threads.delete, + ) + + @cached_property + def messages(self) -> AsyncMessagesResourceWithRawResponse: + return AsyncMessagesResourceWithRawResponse(self._threads.messages) + + @cached_property + def runs(self) -> AsyncRunsResourceWithRawResponse: + return AsyncRunsResourceWithRawResponse(self._threads.runs) + + +class ThreadsResourceWithStreamingResponse: + def __init__(self, threads: ThreadsResource) -> None: + self._threads = threads + + self.create = to_streamed_response_wrapper( + threads.create, + ) + self.retrieve = to_streamed_response_wrapper( + threads.retrieve, + ) + self.list = to_streamed_response_wrapper( + threads.list, + ) + self.delete = to_streamed_response_wrapper( + threads.delete, + ) + + @cached_property + def messages(self) -> MessagesResourceWithStreamingResponse: + return MessagesResourceWithStreamingResponse(self._threads.messages) + + @cached_property + def runs(self) -> RunsResourceWithStreamingResponse: + return RunsResourceWithStreamingResponse(self._threads.runs) + + +class AsyncThreadsResourceWithStreamingResponse: + def __init__(self, threads: AsyncThreadsResource) -> None: + self._threads = threads + + self.create = async_to_streamed_response_wrapper( + threads.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + threads.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + threads.list, + ) + self.delete = async_to_streamed_response_wrapper( + threads.delete, + ) + + @cached_property + def messages(self) -> AsyncMessagesResourceWithStreamingResponse: + return AsyncMessagesResourceWithStreamingResponse(self._threads.messages) + + @cached_property + def runs(self) -> AsyncRunsResourceWithStreamingResponse: + return AsyncRunsResourceWithStreamingResponse(self._threads.runs) diff --git a/src/agility/resources/users/__init__.py b/src/agility/resources/users/__init__.py new file mode 100644 index 0000000..2f30a21 --- /dev/null +++ b/src/agility/resources/users/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .users import ( + UsersResource, + AsyncUsersResource, + UsersResourceWithRawResponse, + AsyncUsersResourceWithRawResponse, + UsersResourceWithStreamingResponse, + AsyncUsersResourceWithStreamingResponse, +) +from .api_key import ( + APIKeyResource, + AsyncAPIKeyResource, + APIKeyResourceWithRawResponse, + AsyncAPIKeyResourceWithRawResponse, + APIKeyResourceWithStreamingResponse, + AsyncAPIKeyResourceWithStreamingResponse, +) + +__all__ = [ + "APIKeyResource", + "AsyncAPIKeyResource", + "APIKeyResourceWithRawResponse", + "AsyncAPIKeyResourceWithRawResponse", + "APIKeyResourceWithStreamingResponse", + "AsyncAPIKeyResourceWithStreamingResponse", + "UsersResource", + "AsyncUsersResource", + "UsersResourceWithRawResponse", + "AsyncUsersResourceWithRawResponse", + "UsersResourceWithStreamingResponse", + "AsyncUsersResourceWithStreamingResponse", +] diff --git a/src/agility/resources/users/api_key.py b/src/agility/resources/users/api_key.py new file mode 100644 index 0000000..c21e5ff --- /dev/null +++ b/src/agility/resources/users/api_key.py @@ -0,0 +1,241 @@ +# 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 ...types.user import User +from ..._base_client import make_request_options + +__all__ = ["APIKeyResource", "AsyncAPIKeyResource"] + + +class APIKeyResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> APIKeyResourceWithRawResponse: + """ + 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers + """ + return APIKeyResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> APIKeyResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/agility-python#with_streaming_response + """ + return APIKeyResourceWithStreamingResponse(self) + + def retrieve( + self, + user_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> str: + """ + Get the API key for a user by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not user_id: + raise ValueError(f"Expected a non-empty value for `user_id` but received {user_id!r}") + return self._get( + f"/api/users/{user_id}/api-key", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=str, + ) + + def refresh( + self, + user_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> User: + """ + Refresh the API key for a user by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not user_id: + raise ValueError(f"Expected a non-empty value for `user_id` but received {user_id!r}") + return self._post( + f"/api/users/{user_id}/api-key/refresh", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=User, + ) + + +class AsyncAPIKeyResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncAPIKeyResourceWithRawResponse: + """ + 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers + """ + return AsyncAPIKeyResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncAPIKeyResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/agility-python#with_streaming_response + """ + return AsyncAPIKeyResourceWithStreamingResponse(self) + + async def retrieve( + self, + user_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> str: + """ + Get the API key for a user by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not user_id: + raise ValueError(f"Expected a non-empty value for `user_id` but received {user_id!r}") + return await self._get( + f"/api/users/{user_id}/api-key", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=str, + ) + + async def refresh( + self, + user_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> User: + """ + Refresh the API key for a user by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not user_id: + raise ValueError(f"Expected a non-empty value for `user_id` but received {user_id!r}") + return await self._post( + f"/api/users/{user_id}/api-key/refresh", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=User, + ) + + +class APIKeyResourceWithRawResponse: + def __init__(self, api_key: APIKeyResource) -> None: + self._api_key = api_key + + self.retrieve = to_raw_response_wrapper( + api_key.retrieve, + ) + self.refresh = to_raw_response_wrapper( + api_key.refresh, + ) + + +class AsyncAPIKeyResourceWithRawResponse: + def __init__(self, api_key: AsyncAPIKeyResource) -> None: + self._api_key = api_key + + self.retrieve = async_to_raw_response_wrapper( + api_key.retrieve, + ) + self.refresh = async_to_raw_response_wrapper( + api_key.refresh, + ) + + +class APIKeyResourceWithStreamingResponse: + def __init__(self, api_key: APIKeyResource) -> None: + self._api_key = api_key + + self.retrieve = to_streamed_response_wrapper( + api_key.retrieve, + ) + self.refresh = to_streamed_response_wrapper( + api_key.refresh, + ) + + +class AsyncAPIKeyResourceWithStreamingResponse: + def __init__(self, api_key: AsyncAPIKeyResource) -> None: + self._api_key = api_key + + self.retrieve = async_to_streamed_response_wrapper( + api_key.retrieve, + ) + self.refresh = async_to_streamed_response_wrapper( + api_key.refresh, + ) diff --git a/src/agility/resources/users/users.py b/src/agility/resources/users/users.py new file mode 100644 index 0000000..16a7b3d --- /dev/null +++ b/src/agility/resources/users/users.py @@ -0,0 +1,195 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from .api_key import ( + APIKeyResource, + AsyncAPIKeyResource, + APIKeyResourceWithRawResponse, + AsyncAPIKeyResourceWithRawResponse, + APIKeyResourceWithStreamingResponse, + AsyncAPIKeyResourceWithStreamingResponse, +) +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 ...types.user import User +from ..._base_client import make_request_options + +__all__ = ["UsersResource", "AsyncUsersResource"] + + +class UsersResource(SyncAPIResource): + @cached_property + def api_key(self) -> APIKeyResource: + return APIKeyResource(self._client) + + @cached_property + def with_raw_response(self) -> UsersResourceWithRawResponse: + """ + 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers + """ + return UsersResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> UsersResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/agility-python#with_streaming_response + """ + return UsersResourceWithStreamingResponse(self) + + def retrieve( + self, + user_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> User: + """ + Get a user by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not user_id: + raise ValueError(f"Expected a non-empty value for `user_id` but received {user_id!r}") + return self._get( + f"/api/users/{user_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=User, + ) + + +class AsyncUsersResource(AsyncAPIResource): + @cached_property + def api_key(self) -> AsyncAPIKeyResource: + return AsyncAPIKeyResource(self._client) + + @cached_property + def with_raw_response(self) -> AsyncUsersResourceWithRawResponse: + """ + 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers + """ + return AsyncUsersResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncUsersResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/agility-python#with_streaming_response + """ + return AsyncUsersResourceWithStreamingResponse(self) + + async def retrieve( + self, + user_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> User: + """ + Get a user by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not user_id: + raise ValueError(f"Expected a non-empty value for `user_id` but received {user_id!r}") + return await self._get( + f"/api/users/{user_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=User, + ) + + +class UsersResourceWithRawResponse: + def __init__(self, users: UsersResource) -> None: + self._users = users + + self.retrieve = to_raw_response_wrapper( + users.retrieve, + ) + + @cached_property + def api_key(self) -> APIKeyResourceWithRawResponse: + return APIKeyResourceWithRawResponse(self._users.api_key) + + +class AsyncUsersResourceWithRawResponse: + def __init__(self, users: AsyncUsersResource) -> None: + self._users = users + + self.retrieve = async_to_raw_response_wrapper( + users.retrieve, + ) + + @cached_property + def api_key(self) -> AsyncAPIKeyResourceWithRawResponse: + return AsyncAPIKeyResourceWithRawResponse(self._users.api_key) + + +class UsersResourceWithStreamingResponse: + def __init__(self, users: UsersResource) -> None: + self._users = users + + self.retrieve = to_streamed_response_wrapper( + users.retrieve, + ) + + @cached_property + def api_key(self) -> APIKeyResourceWithStreamingResponse: + return APIKeyResourceWithStreamingResponse(self._users.api_key) + + +class AsyncUsersResourceWithStreamingResponse: + def __init__(self, users: AsyncUsersResource) -> None: + self._users = users + + self.retrieve = async_to_streamed_response_wrapper( + users.retrieve, + ) + + @cached_property + def api_key(self) -> AsyncAPIKeyResourceWithStreamingResponse: + return AsyncAPIKeyResourceWithStreamingResponse(self._users.api_key) diff --git a/src/agility/types/__init__.py b/src/agility/types/__init__.py new file mode 100644 index 0000000..791d45a --- /dev/null +++ b/src/agility/types/__init__.py @@ -0,0 +1,20 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .user import User as User +from .thread import Thread as Thread +from .assistant import Assistant as Assistant +from .thread_list_params import ThreadListParams as ThreadListParams +from .thread_list_response import ThreadListResponse as ThreadListResponse +from .assistant_list_params import AssistantListParams as AssistantListParams +from .assistant_with_config import AssistantWithConfig as AssistantWithConfig +from .health_check_response import HealthCheckResponse as HealthCheckResponse +from .assistant_create_params import AssistantCreateParams as AssistantCreateParams +from .assistant_list_response import AssistantListResponse as AssistantListResponse +from .assistant_update_params import AssistantUpdateParams as AssistantUpdateParams +from .knowledge_base_list_params import KnowledgeBaseListParams as KnowledgeBaseListParams +from .knowledge_base_with_config import KnowledgeBaseWithConfig as KnowledgeBaseWithConfig +from .knowledge_base_create_params import KnowledgeBaseCreateParams as KnowledgeBaseCreateParams +from .knowledge_base_list_response import KnowledgeBaseListResponse as KnowledgeBaseListResponse +from .knowledge_base_update_params import KnowledgeBaseUpdateParams as KnowledgeBaseUpdateParams diff --git a/src/agility/types/assistant.py b/src/agility/types/assistant.py new file mode 100644 index 0000000..50b13df --- /dev/null +++ b/src/agility/types/assistant.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__ = ["Assistant"] + + +class Assistant(BaseModel): + id: str + + created_at: datetime + + deleted_at: Optional[datetime] = None + + description: str + + name: str + + updated_at: datetime diff --git a/src/agility/types/assistant_create_params.py b/src/agility/types/assistant_create_params.py new file mode 100644 index 0000000..e105b20 --- /dev/null +++ b/src/agility/types/assistant_create_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 import Optional +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["AssistantCreateParams"] + + +class AssistantCreateParams(TypedDict, total=False): + description: Required[str] + + knowledge_base_id: Required[str] + + name: Required[str] + + instructions: Optional[str] + + model: Optional[Literal["gpt-4o"]] diff --git a/src/agility/types/assistant_list_params.py b/src/agility/types/assistant_list_params.py new file mode 100644 index 0000000..0c398a5 --- /dev/null +++ b/src/agility/types/assistant_list_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 TypedDict + +__all__ = ["AssistantListParams"] + + +class AssistantListParams(TypedDict, total=False): + limit: int + + offset: int diff --git a/src/agility/types/assistant_list_response.py b/src/agility/types/assistant_list_response.py new file mode 100644 index 0000000..522859c --- /dev/null +++ b/src/agility/types/assistant_list_response.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List +from typing_extensions import TypeAlias + +from .assistant_with_config import AssistantWithConfig + +__all__ = ["AssistantListResponse"] + +AssistantListResponse: TypeAlias = List[AssistantWithConfig] diff --git a/src/agility/types/assistant_update_params.py b/src/agility/types/assistant_update_params.py new file mode 100644 index 0000000..33fe30b --- /dev/null +++ b/src/agility/types/assistant_update_params.py @@ -0,0 +1,22 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["AssistantUpdateParams"] + + +class AssistantUpdateParams(TypedDict, total=False): + id: Required[str] + + description: Required[str] + + knowledge_base_id: Required[str] + + name: Required[str] + + instructions: Optional[str] + + model: Optional[Literal["gpt-4o"]] diff --git a/src/agility/types/assistant_with_config.py b/src/agility/types/assistant_with_config.py new file mode 100644 index 0000000..4fe0d2f --- /dev/null +++ b/src/agility/types/assistant_with_config.py @@ -0,0 +1,29 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["AssistantWithConfig"] + + +class AssistantWithConfig(BaseModel): + id: str + + created_at: datetime + + deleted_at: Optional[datetime] = None + + description: str + + knowledge_base_id: str + + name: str + + updated_at: datetime + + instructions: Optional[str] = None + + model: Optional[Literal["gpt-4o"]] = None diff --git a/src/agility/types/assistants/__init__.py b/src/agility/types/assistants/__init__.py new file mode 100644 index 0000000..cf947bf --- /dev/null +++ b/src/agility/types/assistants/__init__.py @@ -0,0 +1,8 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .access_key import AccessKey as AccessKey +from .access_key_list_params import AccessKeyListParams as AccessKeyListParams +from .access_key_create_params import AccessKeyCreateParams as AccessKeyCreateParams +from .access_key_list_response import AccessKeyListResponse as AccessKeyListResponse diff --git a/src/agility/types/assistants/access_key.py b/src/agility/types/assistants/access_key.py new file mode 100644 index 0000000..6a9b48b --- /dev/null +++ b/src/agility/types/assistants/access_key.py @@ -0,0 +1,33 @@ +# 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__ = ["AccessKey"] + + +class AccessKey(BaseModel): + id: str + + token: str + + assistant_id: str + + created_at: datetime + + creator_user_id: str + + description: Optional[str] = None + + expires_at: Optional[datetime] = None + + last_used_at: Optional[datetime] = None + + name: str + + status: Literal["active", "expired", "inactive", "revoked"] + + updated_at: datetime diff --git a/src/agility/types/assistants/access_key_create_params.py b/src/agility/types/assistants/access_key_create_params.py new file mode 100644 index 0000000..1cbc202 --- /dev/null +++ b/src/agility/types/assistants/access_key_create_params.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union, Optional +from datetime import datetime +from typing_extensions import Required, Annotated, TypedDict + +from ..._utils import PropertyInfo + +__all__ = ["AccessKeyCreateParams"] + + +class AccessKeyCreateParams(TypedDict, total=False): + name: Required[str] + + description: Optional[str] + + expires_at: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")] diff --git a/src/agility/types/assistants/access_key_list_params.py b/src/agility/types/assistants/access_key_list_params.py new file mode 100644 index 0000000..c1337d0 --- /dev/null +++ b/src/agility/types/assistants/access_key_list_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 TypedDict + +__all__ = ["AccessKeyListParams"] + + +class AccessKeyListParams(TypedDict, total=False): + limit: int + + offset: int diff --git a/src/agility/types/assistants/access_key_list_response.py b/src/agility/types/assistants/access_key_list_response.py new file mode 100644 index 0000000..0a33333 --- /dev/null +++ b/src/agility/types/assistants/access_key_list_response.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List +from typing_extensions import TypeAlias + +from .access_key import AccessKey + +__all__ = ["AccessKeyListResponse"] + +AccessKeyListResponse: TypeAlias = List[AccessKey] diff --git a/src/agility/types/health_check_response.py b/src/agility/types/health_check_response.py new file mode 100644 index 0000000..5303b51 --- /dev/null +++ b/src/agility/types/health_check_response.py @@ -0,0 +1,11 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + + + +from .._models import BaseModel + +__all__ = ["HealthCheckResponse"] + + +class HealthCheckResponse(BaseModel): + status: str diff --git a/src/agility/types/knowledge_base_create_params.py b/src/agility/types/knowledge_base_create_params.py new file mode 100644 index 0000000..5f823bf --- /dev/null +++ b/src/agility/types/knowledge_base_create_params.py @@ -0,0 +1,114 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict, Union +from typing_extensions import Literal, Required, TypeAlias, TypedDict + +__all__ = [ + "KnowledgeBaseCreateParams", + "IngestionPipelineParams", + "IngestionPipelineParamsCurate", + "IngestionPipelineParamsCurateSteps", + "IngestionPipelineParamsCurateStepsRemoveExactDuplicatesParams", + "IngestionPipelineParamsCurateStepsTagExactDuplicatesParams", + "IngestionPipelineParamsCurateStepsPostpendContentParams", + "IngestionPipelineParamsCurateDocumentStore", + "IngestionPipelineParamsTransform", + "IngestionPipelineParamsTransformSteps", + "IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params", + "IngestionPipelineParamsTransformStepsNoopParams", + "IngestionPipelineParamsVectorStore", +] + + +class KnowledgeBaseCreateParams(TypedDict, total=False): + description: Required[str] + + ingestion_pipeline_params: Required[IngestionPipelineParams] + """Knowledge base pipeline params. + + Parameters defined on the knowledge-base level for a pipeline. + """ + + name: Required[str] + + +class IngestionPipelineParamsCurateStepsRemoveExactDuplicatesParams(TypedDict, total=False): + name: Literal["remove_exact_duplicates.v0"] + + +class IngestionPipelineParamsCurateStepsTagExactDuplicatesParams(TypedDict, total=False): + name: Literal["tag_exact_duplicates.v0"] + + +class IngestionPipelineParamsCurateStepsPostpendContentParams(TypedDict, total=False): + postpend_value: Required[str] + """The value to postpend to the content.""" + + name: Literal["postpend_content.v0"] + + +IngestionPipelineParamsCurateSteps: TypeAlias = Union[ + IngestionPipelineParamsCurateStepsRemoveExactDuplicatesParams, + IngestionPipelineParamsCurateStepsTagExactDuplicatesParams, + IngestionPipelineParamsCurateStepsPostpendContentParams, +] + + +class IngestionPipelineParamsCurate(TypedDict, total=False): + steps: Dict[str, IngestionPipelineParamsCurateSteps] + + +class IngestionPipelineParamsCurateDocumentStore(TypedDict, total=False): + document_tags: Dict[str, str] + + +class IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params(TypedDict, total=False): + chunk_overlap: int + + chunk_size: int + + name: Literal["splitters.recursive_character.v0"] + + +class IngestionPipelineParamsTransformStepsNoopParams(TypedDict, total=False): + name: Literal["noop"] + + +IngestionPipelineParamsTransformSteps: TypeAlias = Union[ + IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params, + IngestionPipelineParamsTransformStepsNoopParams, +] + + +class IngestionPipelineParamsTransform(TypedDict, total=False): + steps: Dict[str, IngestionPipelineParamsTransformSteps] + + +class IngestionPipelineParamsVectorStore(TypedDict, total=False): + weaviate_collection_name: Required[str] + + node_tags: Dict[str, str] + + +class IngestionPipelineParams(TypedDict, total=False): + curate: Required[IngestionPipelineParamsCurate] + """Curate params. + + Defines full curation pipeline, as an ordered dict of named curation steps. + Order of steps _does_ matter -- they are executed in the order defined. + """ + + curate_document_store: Required[IngestionPipelineParamsCurateDocumentStore] + """Document store params.""" + + transform: Required[IngestionPipelineParamsTransform] + """Transform params. + + Defines full transform pipeline, as an ordered dict of named transform steps. + Order of steps _does_ matter -- they are executed in the order defined. + """ + + vector_store: Required[IngestionPipelineParamsVectorStore] + """Vector store params.""" diff --git a/src/agility/types/knowledge_base_list_params.py b/src/agility/types/knowledge_base_list_params.py new file mode 100644 index 0000000..76db384 --- /dev/null +++ b/src/agility/types/knowledge_base_list_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 TypedDict + +__all__ = ["KnowledgeBaseListParams"] + + +class KnowledgeBaseListParams(TypedDict, total=False): + limit: int + + offset: int diff --git a/src/agility/types/knowledge_base_list_response.py b/src/agility/types/knowledge_base_list_response.py new file mode 100644 index 0000000..b1ef591 --- /dev/null +++ b/src/agility/types/knowledge_base_list_response.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List +from typing_extensions import TypeAlias + +from .knowledge_base_with_config import KnowledgeBaseWithConfig + +__all__ = ["KnowledgeBaseListResponse"] + +KnowledgeBaseListResponse: TypeAlias = List[KnowledgeBaseWithConfig] diff --git a/src/agility/types/knowledge_base_update_params.py b/src/agility/types/knowledge_base_update_params.py new file mode 100644 index 0000000..68c055b --- /dev/null +++ b/src/agility/types/knowledge_base_update_params.py @@ -0,0 +1,114 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict, Union +from typing_extensions import Literal, Required, TypeAlias, TypedDict + +__all__ = [ + "KnowledgeBaseUpdateParams", + "IngestionPipelineParams", + "IngestionPipelineParamsCurate", + "IngestionPipelineParamsCurateSteps", + "IngestionPipelineParamsCurateStepsRemoveExactDuplicatesParams", + "IngestionPipelineParamsCurateStepsTagExactDuplicatesParams", + "IngestionPipelineParamsCurateStepsPostpendContentParams", + "IngestionPipelineParamsCurateDocumentStore", + "IngestionPipelineParamsTransform", + "IngestionPipelineParamsTransformSteps", + "IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params", + "IngestionPipelineParamsTransformStepsNoopParams", + "IngestionPipelineParamsVectorStore", +] + + +class KnowledgeBaseUpdateParams(TypedDict, total=False): + description: Required[str] + + ingestion_pipeline_params: Required[IngestionPipelineParams] + """Knowledge base pipeline params. + + Parameters defined on the knowledge-base level for a pipeline. + """ + + name: Required[str] + + +class IngestionPipelineParamsCurateStepsRemoveExactDuplicatesParams(TypedDict, total=False): + name: Literal["remove_exact_duplicates.v0"] + + +class IngestionPipelineParamsCurateStepsTagExactDuplicatesParams(TypedDict, total=False): + name: Literal["tag_exact_duplicates.v0"] + + +class IngestionPipelineParamsCurateStepsPostpendContentParams(TypedDict, total=False): + postpend_value: Required[str] + """The value to postpend to the content.""" + + name: Literal["postpend_content.v0"] + + +IngestionPipelineParamsCurateSteps: TypeAlias = Union[ + IngestionPipelineParamsCurateStepsRemoveExactDuplicatesParams, + IngestionPipelineParamsCurateStepsTagExactDuplicatesParams, + IngestionPipelineParamsCurateStepsPostpendContentParams, +] + + +class IngestionPipelineParamsCurate(TypedDict, total=False): + steps: Dict[str, IngestionPipelineParamsCurateSteps] + + +class IngestionPipelineParamsCurateDocumentStore(TypedDict, total=False): + document_tags: Dict[str, str] + + +class IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params(TypedDict, total=False): + chunk_overlap: int + + chunk_size: int + + name: Literal["splitters.recursive_character.v0"] + + +class IngestionPipelineParamsTransformStepsNoopParams(TypedDict, total=False): + name: Literal["noop"] + + +IngestionPipelineParamsTransformSteps: TypeAlias = Union[ + IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params, + IngestionPipelineParamsTransformStepsNoopParams, +] + + +class IngestionPipelineParamsTransform(TypedDict, total=False): + steps: Dict[str, IngestionPipelineParamsTransformSteps] + + +class IngestionPipelineParamsVectorStore(TypedDict, total=False): + weaviate_collection_name: Required[str] + + node_tags: Dict[str, str] + + +class IngestionPipelineParams(TypedDict, total=False): + curate: Required[IngestionPipelineParamsCurate] + """Curate params. + + Defines full curation pipeline, as an ordered dict of named curation steps. + Order of steps _does_ matter -- they are executed in the order defined. + """ + + curate_document_store: Required[IngestionPipelineParamsCurateDocumentStore] + """Document store params.""" + + transform: Required[IngestionPipelineParamsTransform] + """Transform params. + + Defines full transform pipeline, as an ordered dict of named transform steps. + Order of steps _does_ matter -- they are executed in the order defined. + """ + + vector_store: Required[IngestionPipelineParamsVectorStore] + """Vector store params.""" diff --git a/src/agility/types/knowledge_base_with_config.py b/src/agility/types/knowledge_base_with_config.py new file mode 100644 index 0000000..9fe414c --- /dev/null +++ b/src/agility/types/knowledge_base_with_config.py @@ -0,0 +1,127 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, Union, Optional +from datetime import datetime +from typing_extensions import Literal, Annotated, TypeAlias + +from .._utils import PropertyInfo +from .._models import BaseModel + +__all__ = [ + "KnowledgeBaseWithConfig", + "IngestionPipelineParams", + "IngestionPipelineParamsCurate", + "IngestionPipelineParamsCurateSteps", + "IngestionPipelineParamsCurateStepsRemoveExactDuplicatesParams", + "IngestionPipelineParamsCurateStepsTagExactDuplicatesParams", + "IngestionPipelineParamsCurateStepsPostpendContentParams", + "IngestionPipelineParamsCurateDocumentStore", + "IngestionPipelineParamsTransform", + "IngestionPipelineParamsTransformSteps", + "IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params", + "IngestionPipelineParamsTransformStepsNoopParams", + "IngestionPipelineParamsVectorStore", +] + + +class IngestionPipelineParamsCurateStepsRemoveExactDuplicatesParams(BaseModel): + name: Optional[Literal["remove_exact_duplicates.v0"]] = None + + +class IngestionPipelineParamsCurateStepsTagExactDuplicatesParams(BaseModel): + name: Optional[Literal["tag_exact_duplicates.v0"]] = None + + +class IngestionPipelineParamsCurateStepsPostpendContentParams(BaseModel): + postpend_value: str + """The value to postpend to the content.""" + + name: Optional[Literal["postpend_content.v0"]] = None + + +IngestionPipelineParamsCurateSteps: TypeAlias = Annotated[ + Union[ + IngestionPipelineParamsCurateStepsRemoveExactDuplicatesParams, + IngestionPipelineParamsCurateStepsTagExactDuplicatesParams, + IngestionPipelineParamsCurateStepsPostpendContentParams, + ], + PropertyInfo(discriminator="name"), +] + + +class IngestionPipelineParamsCurate(BaseModel): + steps: Optional[Dict[str, IngestionPipelineParamsCurateSteps]] = None + + +class IngestionPipelineParamsCurateDocumentStore(BaseModel): + document_tags: Optional[Dict[str, str]] = None + + +class IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params(BaseModel): + chunk_overlap: Optional[int] = None + + chunk_size: Optional[int] = None + + name: Optional[Literal["splitters.recursive_character.v0"]] = None + + +class IngestionPipelineParamsTransformStepsNoopParams(BaseModel): + name: Optional[Literal["noop"]] = None + + +IngestionPipelineParamsTransformSteps: TypeAlias = Union[ + IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params, + IngestionPipelineParamsTransformStepsNoopParams, +] + + +class IngestionPipelineParamsTransform(BaseModel): + steps: Optional[Dict[str, IngestionPipelineParamsTransformSteps]] = None + + +class IngestionPipelineParamsVectorStore(BaseModel): + weaviate_collection_name: str + + node_tags: Optional[Dict[str, str]] = None + + +class IngestionPipelineParams(BaseModel): + curate: IngestionPipelineParamsCurate + """Curate params. + + Defines full curation pipeline, as an ordered dict of named curation steps. + Order of steps _does_ matter -- they are executed in the order defined. + """ + + curate_document_store: IngestionPipelineParamsCurateDocumentStore + """Document store params.""" + + transform: IngestionPipelineParamsTransform + """Transform params. + + Defines full transform pipeline, as an ordered dict of named transform steps. + Order of steps _does_ matter -- they are executed in the order defined. + """ + + vector_store: IngestionPipelineParamsVectorStore + """Vector store params.""" + + +class KnowledgeBaseWithConfig(BaseModel): + id: str + + created_at: datetime + + deleted_at: Optional[datetime] = None + + description: str + + ingestion_pipeline_params: IngestionPipelineParams + """Knowledge base pipeline params. + + Parameters defined on the knowledge-base level for a pipeline. + """ + + name: str + + updated_at: datetime diff --git a/src/agility/types/knowledge_bases/__init__.py b/src/agility/types/knowledge_bases/__init__.py new file mode 100644 index 0000000..dffb0ea --- /dev/null +++ b/src/agility/types/knowledge_bases/__init__.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .source import Source as Source +from .source_list_params import SourceListParams as SourceListParams +from .source_create_params import SourceCreateParams as SourceCreateParams +from .source_list_response import SourceListResponse as SourceListResponse +from .source_update_params import SourceUpdateParams as SourceUpdateParams +from .source_status_response import SourceStatusResponse as SourceStatusResponse diff --git a/src/agility/types/knowledge_bases/source.py b/src/agility/types/knowledge_bases/source.py new file mode 100644 index 0000000..620955b --- /dev/null +++ b/src/agility/types/knowledge_bases/source.py @@ -0,0 +1,68 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Union, Optional +from datetime import datetime +from typing_extensions import Literal, Annotated, TypeAlias + +from ..._utils import PropertyInfo +from ..._models import BaseModel + +__all__ = ["Source", "SourceParams", "SourceParamsWebV0Params", "SourceParamsNotionParams", "SourceSchedule"] + + +class SourceParamsWebV0Params(BaseModel): + urls: List[str] + + allow_backward_links: Optional[bool] = None + + allow_external_links: Optional[bool] = None + + exclude_regex: Optional[str] = None + + include_regex: Optional[str] = None + + limit: Optional[int] = None + + max_depth: Optional[int] = None + + name: Optional[Literal["web_v0"]] = None + + +class SourceParamsNotionParams(BaseModel): + name: Optional[Literal["notion"]] = None + + +SourceParams: TypeAlias = Annotated[ + Union[SourceParamsWebV0Params, SourceParamsNotionParams], PropertyInfo(discriminator="name") +] + + +class SourceSchedule(BaseModel): + cron: str + + utc_offset: int + + +class Source(BaseModel): + id: str + + created_at: datetime + + deleted_at: Optional[datetime] = None + + description: str + + knowledge_base_id: str + + name: str + + source_params: SourceParams + """Parameters for web v0 sources.""" + + source_schedule: SourceSchedule + """Source schedule model.""" + + status: Literal["pending", "syncing", "synced", "failed"] + """Source status enum.""" + + updated_at: datetime diff --git a/src/agility/types/knowledge_bases/source_create_params.py b/src/agility/types/knowledge_bases/source_create_params.py new file mode 100644 index 0000000..ba1daf0 --- /dev/null +++ b/src/agility/types/knowledge_bases/source_create_params.py @@ -0,0 +1,57 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Union, Optional +from typing_extensions import Literal, Required, TypeAlias, TypedDict + +__all__ = [ + "SourceCreateParams", + "SourceParams", + "SourceParamsWebV0Params", + "SourceParamsNotionParams", + "SourceSchedule", +] + + +class SourceCreateParams(TypedDict, total=False): + description: Required[str] + + name: Required[str] + + source_params: Required[SourceParams] + """Parameters for web v0 sources.""" + + source_schedule: Required[SourceSchedule] + """Source schedule model.""" + + +class SourceParamsWebV0Params(TypedDict, total=False): + urls: Required[List[str]] + + allow_backward_links: bool + + allow_external_links: bool + + exclude_regex: Optional[str] + + include_regex: Optional[str] + + limit: int + + max_depth: int + + name: Literal["web_v0"] + + +class SourceParamsNotionParams(TypedDict, total=False): + name: Literal["notion"] + + +SourceParams: TypeAlias = Union[SourceParamsWebV0Params, SourceParamsNotionParams] + + +class SourceSchedule(TypedDict, total=False): + cron: Required[str] + + utc_offset: Required[int] diff --git a/src/agility/types/knowledge_bases/source_list_params.py b/src/agility/types/knowledge_bases/source_list_params.py new file mode 100644 index 0000000..9cd0333 --- /dev/null +++ b/src/agility/types/knowledge_bases/source_list_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 TypedDict + +__all__ = ["SourceListParams"] + + +class SourceListParams(TypedDict, total=False): + limit: int + + offset: int diff --git a/src/agility/types/knowledge_bases/source_list_response.py b/src/agility/types/knowledge_bases/source_list_response.py new file mode 100644 index 0000000..3029f00 --- /dev/null +++ b/src/agility/types/knowledge_bases/source_list_response.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List +from typing_extensions import TypeAlias + +from .source import Source + +__all__ = ["SourceListResponse"] + +SourceListResponse: TypeAlias = List[Source] diff --git a/src/agility/types/knowledge_bases/source_status_response.py b/src/agility/types/knowledge_bases/source_status_response.py new file mode 100644 index 0000000..99f383b --- /dev/null +++ b/src/agility/types/knowledge_bases/source_status_response.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from datetime import datetime +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["SourceStatusResponse"] + + +class SourceStatusResponse(BaseModel): + status: Literal["pending", "syncing", "synced", "failed"] + """Source status enum.""" + + updated_at: datetime diff --git a/src/agility/types/knowledge_bases/source_update_params.py b/src/agility/types/knowledge_bases/source_update_params.py new file mode 100644 index 0000000..dd26d39 --- /dev/null +++ b/src/agility/types/knowledge_bases/source_update_params.py @@ -0,0 +1,59 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Union, Optional +from typing_extensions import Literal, Required, TypeAlias, TypedDict + +__all__ = [ + "SourceUpdateParams", + "SourceParams", + "SourceParamsWebV0Params", + "SourceParamsNotionParams", + "SourceSchedule", +] + + +class SourceUpdateParams(TypedDict, total=False): + knowledge_base_id: Required[str] + + description: Required[str] + + name: Required[str] + + source_params: Required[SourceParams] + """Parameters for web v0 sources.""" + + source_schedule: Required[SourceSchedule] + """Source schedule model.""" + + +class SourceParamsWebV0Params(TypedDict, total=False): + urls: Required[List[str]] + + allow_backward_links: bool + + allow_external_links: bool + + exclude_regex: Optional[str] + + include_regex: Optional[str] + + limit: int + + max_depth: int + + name: Literal["web_v0"] + + +class SourceParamsNotionParams(TypedDict, total=False): + name: Literal["notion"] + + +SourceParams: TypeAlias = Union[SourceParamsWebV0Params, SourceParamsNotionParams] + + +class SourceSchedule(TypedDict, total=False): + cron: Required[str] + + utc_offset: Required[int] diff --git a/src/agility/types/knowledge_bases/sources/__init__.py b/src/agility/types/knowledge_bases/sources/__init__.py new file mode 100644 index 0000000..6ae1117 --- /dev/null +++ b/src/agility/types/knowledge_bases/sources/__init__.py @@ -0,0 +1,7 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .document import Document as Document +from .document_list_params import DocumentListParams as DocumentListParams +from .document_list_response import DocumentListResponse as DocumentListResponse diff --git a/src/agility/types/knowledge_bases/sources/document.py b/src/agility/types/knowledge_bases/sources/document.py new file mode 100644 index 0000000..93eb686 --- /dev/null +++ b/src/agility/types/knowledge_bases/sources/document.py @@ -0,0 +1,34 @@ +# 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__ = ["Document", "Metadata"] + + +class Metadata(BaseModel): + key: str + + value: str + + +class Document(BaseModel): + id: str + + content: str + + created_at: datetime + + deleted_at: Optional[datetime] = None + + knowledge_base_id: str + + metadata: List[Metadata] + + source_id: str + + title: str + + updated_at: datetime diff --git a/src/agility/types/knowledge_bases/sources/document_list_params.py b/src/agility/types/knowledge_bases/sources/document_list_params.py new file mode 100644 index 0000000..f4f893f --- /dev/null +++ b/src/agility/types/knowledge_bases/sources/document_list_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["DocumentListParams"] + + +class DocumentListParams(TypedDict, total=False): + knowledge_base_id: Required[str] + + limit: int + + offset: int diff --git a/src/agility/types/knowledge_bases/sources/document_list_response.py b/src/agility/types/knowledge_bases/sources/document_list_response.py new file mode 100644 index 0000000..5dd42b3 --- /dev/null +++ b/src/agility/types/knowledge_bases/sources/document_list_response.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List +from typing_extensions import TypeAlias + +from .document import Document + +__all__ = ["DocumentListResponse"] + +DocumentListResponse: TypeAlias = List[Document] diff --git a/src/agility/types/thread.py b/src/agility/types/thread.py new file mode 100644 index 0000000..f9f7a43 --- /dev/null +++ b/src/agility/types/thread.py @@ -0,0 +1,20 @@ +# 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__ = ["Thread"] + + +class Thread(BaseModel): + id: str + + created_at: datetime + + deleted_at: Optional[datetime] = None + + updated_at: datetime + + user_id: Optional[str] = None diff --git a/src/agility/types/thread_list_params.py b/src/agility/types/thread_list_params.py new file mode 100644 index 0000000..aeaa03f --- /dev/null +++ b/src/agility/types/thread_list_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 TypedDict + +__all__ = ["ThreadListParams"] + + +class ThreadListParams(TypedDict, total=False): + limit: int + + offset: int diff --git a/src/agility/types/thread_list_response.py b/src/agility/types/thread_list_response.py new file mode 100644 index 0000000..bdd5254 --- /dev/null +++ b/src/agility/types/thread_list_response.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List +from typing_extensions import TypeAlias + +from .thread import Thread + +__all__ = ["ThreadListResponse"] + +ThreadListResponse: TypeAlias = List[Thread] diff --git a/src/agility/types/threads/__init__.py b/src/agility/types/threads/__init__.py new file mode 100644 index 0000000..6b46983 --- /dev/null +++ b/src/agility/types/threads/__init__.py @@ -0,0 +1,11 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .run import Run as Run +from .message import Message as Message +from .run_create_params import RunCreateParams as RunCreateParams +from .run_stream_params import RunStreamParams as RunStreamParams +from .message_list_params import MessageListParams as MessageListParams +from .message_create_params import MessageCreateParams as MessageCreateParams +from .message_list_response import MessageListResponse as MessageListResponse diff --git a/src/agility/types/threads/message.py b/src/agility/types/threads/message.py new file mode 100644 index 0000000..297b74c --- /dev/null +++ b/src/agility/types/threads/message.py @@ -0,0 +1,31 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["Message", "Metadata"] + + +class Metadata(BaseModel): + trustworthiness_score: Optional[float] = None + + +class Message(BaseModel): + id: str + + content: str + + created_at: datetime + + metadata: Metadata + + role: Literal["user", "assistant"] + + thread_id: str + + updated_at: datetime + + deleted_at: Optional[datetime] = None diff --git a/src/agility/types/threads/message_create_params.py b/src/agility/types/threads/message_create_params.py new file mode 100644 index 0000000..d1ec82e --- /dev/null +++ b/src/agility/types/threads/message_create_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 import Optional +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["MessageCreateParams", "Metadata"] + + +class MessageCreateParams(TypedDict, total=False): + content: Required[str] + + metadata: Required[Optional[Metadata]] + + role: Required[Literal["user", "assistant"]] + + +class Metadata(TypedDict, total=False): + trustworthiness_score: Optional[float] diff --git a/src/agility/types/threads/message_list_params.py b/src/agility/types/threads/message_list_params.py new file mode 100644 index 0000000..53a5dc7 --- /dev/null +++ b/src/agility/types/threads/message_list_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 TypedDict + +__all__ = ["MessageListParams"] + + +class MessageListParams(TypedDict, total=False): + limit: int + + offset: int diff --git a/src/agility/types/threads/message_list_response.py b/src/agility/types/threads/message_list_response.py new file mode 100644 index 0000000..b316d11 --- /dev/null +++ b/src/agility/types/threads/message_list_response.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List +from typing_extensions import TypeAlias + +from .message import Message + +__all__ = ["MessageListResponse"] + +MessageListResponse: TypeAlias = List[Message] diff --git a/src/agility/types/threads/run.py b/src/agility/types/threads/run.py new file mode 100644 index 0000000..192adc5 --- /dev/null +++ b/src/agility/types/threads/run.py @@ -0,0 +1,43 @@ +# 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__ = ["Run", "Usage"] + + +class Usage(BaseModel): + completion_tokens: int + + prompt_tokens: int + + total_tokens: int + + +class Run(BaseModel): + id: str + + assistant_id: str + + created_at: datetime + + status: Literal["pending", "in_progress", "completed", "failed", "canceled", "expired"] + + thread_id: str + + updated_at: datetime + + additional_instructions: Optional[str] = None + + deleted_at: Optional[datetime] = None + + instructions: Optional[str] = None + + knowledge_base_id: Optional[str] = None + + model: Optional[Literal["gpt-4o"]] = None + + usage: Optional[Usage] = None diff --git a/src/agility/types/threads/run_create_params.py b/src/agility/types/threads/run_create_params.py new file mode 100644 index 0000000..b42f8a7 --- /dev/null +++ b/src/agility/types/threads/run_create_params.py @@ -0,0 +1,36 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Iterable, Optional +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["RunCreateParams", "AdditionalMessage", "AdditionalMessageMetadata"] + + +class RunCreateParams(TypedDict, total=False): + assistant_id: Required[str] + + additional_instructions: Optional[str] + + additional_messages: Iterable[AdditionalMessage] + + instructions: Optional[str] + + knowledge_base_id: Optional[str] + + model: Optional[Literal["gpt-4o"]] + + +class AdditionalMessageMetadata(TypedDict, total=False): + trustworthiness_score: Optional[float] + + +class AdditionalMessage(TypedDict, total=False): + content: Required[str] + + metadata: Required[AdditionalMessageMetadata] + + role: Required[Literal["user", "assistant"]] + + thread_id: Required[str] diff --git a/src/agility/types/threads/run_stream_params.py b/src/agility/types/threads/run_stream_params.py new file mode 100644 index 0000000..804d30f --- /dev/null +++ b/src/agility/types/threads/run_stream_params.py @@ -0,0 +1,36 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Iterable, Optional +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["RunStreamParams", "AdditionalMessage", "AdditionalMessageMetadata"] + + +class RunStreamParams(TypedDict, total=False): + assistant_id: Required[str] + + additional_instructions: Optional[str] + + additional_messages: Iterable[AdditionalMessage] + + instructions: Optional[str] + + knowledge_base_id: Optional[str] + + model: Optional[Literal["gpt-4o"]] + + +class AdditionalMessageMetadata(TypedDict, total=False): + trustworthiness_score: Optional[float] + + +class AdditionalMessage(TypedDict, total=False): + content: Required[str] + + metadata: Required[AdditionalMessageMetadata] + + role: Required[Literal["user", "assistant"]] + + thread_id: Required[str] diff --git a/src/agility/types/user.py b/src/agility/types/user.py new file mode 100644 index 0000000..40e1acf --- /dev/null +++ b/src/agility/types/user.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from datetime import datetime + +from .._models import BaseModel + +__all__ = ["User"] + + +class User(BaseModel): + id: str + + api_key: str + + created_at: datetime diff --git a/src/agility/types/users/__init__.py b/src/agility/types/users/__init__.py new file mode 100644 index 0000000..db34d14 --- /dev/null +++ b/src/agility/types/users/__init__.py @@ -0,0 +1,5 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .api_key_retrieve_response import APIKeyRetrieveResponse as APIKeyRetrieveResponse diff --git a/src/agility/types/users/api_key_retrieve_response.py b/src/agility/types/users/api_key_retrieve_response.py new file mode 100644 index 0000000..582f299 --- /dev/null +++ b/src/agility/types/users/api_key_retrieve_response.py @@ -0,0 +1,7 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import TypeAlias + +__all__ = ["APIKeyRetrieveResponse"] + +APIKeyRetrieveResponse: TypeAlias = str diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..fd8019a --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/__init__.py b/tests/api_resources/__init__.py new file mode 100644 index 0000000..fd8019a --- /dev/null +++ b/tests/api_resources/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/assistants/__init__.py b/tests/api_resources/assistants/__init__.py new file mode 100644 index 0000000..fd8019a --- /dev/null +++ b/tests/api_resources/assistants/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/assistants/test_access_keys.py b/tests/api_resources/assistants/test_access_keys.py new file mode 100644 index 0000000..0a2ffc0 --- /dev/null +++ b/tests/api_resources/assistants/test_access_keys.py @@ -0,0 +1,317 @@ +# 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 agility import Agility, AsyncAgility +from tests.utils import assert_matches_type +from agility._utils import parse_datetime +from agility.types.assistants import AccessKey, AccessKeyListResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestAccessKeys: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: Agility) -> None: + access_key = client.assistants.access_keys.create( + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + ) + assert_matches_type(AccessKey, access_key, path=["response"]) + + @parametrize + def test_method_create_with_all_params(self, client: Agility) -> None: + access_key = client.assistants.access_keys.create( + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + description="description", + expires_at=parse_datetime("2019-12-27T18:11:19.117Z"), + ) + assert_matches_type(AccessKey, access_key, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Agility) -> None: + response = client.assistants.access_keys.with_raw_response.create( + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + access_key = response.parse() + assert_matches_type(AccessKey, access_key, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Agility) -> None: + with client.assistants.access_keys.with_streaming_response.create( + assistant_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" + + access_key = response.parse() + assert_matches_type(AccessKey, access_key, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_create(self, client: Agility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `assistant_id` but received ''"): + client.assistants.access_keys.with_raw_response.create( + assistant_id="", + name="name", + ) + + @parametrize + def test_method_retrieve(self, client: Agility) -> None: + access_key = client.assistants.access_keys.retrieve( + access_key_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + assistant_id="assistant_id", + ) + assert_matches_type(AccessKey, access_key, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Agility) -> None: + response = client.assistants.access_keys.with_raw_response.retrieve( + access_key_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + assistant_id="assistant_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + access_key = response.parse() + assert_matches_type(AccessKey, access_key, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Agility) -> None: + with client.assistants.access_keys.with_streaming_response.retrieve( + access_key_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + assistant_id="assistant_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + access_key = response.parse() + assert_matches_type(AccessKey, access_key, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Agility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `assistant_id` but received ''"): + client.assistants.access_keys.with_raw_response.retrieve( + access_key_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + assistant_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `access_key_id` but received ''"): + client.assistants.access_keys.with_raw_response.retrieve( + access_key_id="", + assistant_id="assistant_id", + ) + + @parametrize + def test_method_list(self, client: Agility) -> None: + access_key = client.assistants.access_keys.list( + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(AccessKeyListResponse, access_key, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Agility) -> None: + access_key = client.assistants.access_keys.list( + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + limit=1, + offset=0, + ) + assert_matches_type(AccessKeyListResponse, access_key, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Agility) -> None: + response = client.assistants.access_keys.with_raw_response.list( + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + access_key = response.parse() + assert_matches_type(AccessKeyListResponse, access_key, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Agility) -> None: + with client.assistants.access_keys.with_streaming_response.list( + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + access_key = response.parse() + assert_matches_type(AccessKeyListResponse, access_key, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_list(self, client: Agility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `assistant_id` but received ''"): + client.assistants.access_keys.with_raw_response.list( + assistant_id="", + ) + + +class TestAsyncAccessKeys: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_create(self, async_client: AsyncAgility) -> None: + access_key = await async_client.assistants.access_keys.create( + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + ) + assert_matches_type(AccessKey, access_key, path=["response"]) + + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncAgility) -> None: + access_key = await async_client.assistants.access_keys.create( + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + description="description", + expires_at=parse_datetime("2019-12-27T18:11:19.117Z"), + ) + assert_matches_type(AccessKey, access_key, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncAgility) -> None: + response = await async_client.assistants.access_keys.with_raw_response.create( + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + access_key = await response.parse() + assert_matches_type(AccessKey, access_key, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncAgility) -> None: + async with async_client.assistants.access_keys.with_streaming_response.create( + assistant_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" + + access_key = await response.parse() + assert_matches_type(AccessKey, access_key, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_create(self, async_client: AsyncAgility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `assistant_id` but received ''"): + await async_client.assistants.access_keys.with_raw_response.create( + assistant_id="", + name="name", + ) + + @parametrize + async def test_method_retrieve(self, async_client: AsyncAgility) -> None: + access_key = await async_client.assistants.access_keys.retrieve( + access_key_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + assistant_id="assistant_id", + ) + assert_matches_type(AccessKey, access_key, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncAgility) -> None: + response = await async_client.assistants.access_keys.with_raw_response.retrieve( + access_key_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + assistant_id="assistant_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + access_key = await response.parse() + assert_matches_type(AccessKey, access_key, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncAgility) -> None: + async with async_client.assistants.access_keys.with_streaming_response.retrieve( + access_key_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + assistant_id="assistant_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + access_key = await response.parse() + assert_matches_type(AccessKey, access_key, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncAgility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `assistant_id` but received ''"): + await async_client.assistants.access_keys.with_raw_response.retrieve( + access_key_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + assistant_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `access_key_id` but received ''"): + await async_client.assistants.access_keys.with_raw_response.retrieve( + access_key_id="", + assistant_id="assistant_id", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncAgility) -> None: + access_key = await async_client.assistants.access_keys.list( + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(AccessKeyListResponse, access_key, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncAgility) -> None: + access_key = await async_client.assistants.access_keys.list( + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + limit=1, + offset=0, + ) + assert_matches_type(AccessKeyListResponse, access_key, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncAgility) -> None: + response = await async_client.assistants.access_keys.with_raw_response.list( + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + access_key = await response.parse() + assert_matches_type(AccessKeyListResponse, access_key, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncAgility) -> None: + async with async_client.assistants.access_keys.with_streaming_response.list( + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + access_key = await response.parse() + assert_matches_type(AccessKeyListResponse, access_key, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_list(self, async_client: AsyncAgility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `assistant_id` but received ''"): + await async_client.assistants.access_keys.with_raw_response.list( + assistant_id="", + ) diff --git a/tests/api_resources/knowledge_bases/__init__.py b/tests/api_resources/knowledge_bases/__init__.py new file mode 100644 index 0000000..fd8019a --- /dev/null +++ b/tests/api_resources/knowledge_bases/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/knowledge_bases/sources/__init__.py b/tests/api_resources/knowledge_bases/sources/__init__.py new file mode 100644 index 0000000..fd8019a --- /dev/null +++ b/tests/api_resources/knowledge_bases/sources/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/knowledge_bases/sources/test_documents.py b/tests/api_resources/knowledge_bases/sources/test_documents.py new file mode 100644 index 0000000..84fbe9a --- /dev/null +++ b/tests/api_resources/knowledge_bases/sources/test_documents.py @@ -0,0 +1,258 @@ +# 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 agility import Agility, AsyncAgility +from tests.utils import assert_matches_type +from agility.types.knowledge_bases.sources import Document, DocumentListResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestDocuments: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_retrieve(self, client: Agility) -> None: + document = client.knowledge_bases.sources.documents.retrieve( + document_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(Document, document, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Agility) -> None: + response = client.knowledge_bases.sources.documents.with_raw_response.retrieve( + document_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + document = response.parse() + assert_matches_type(Document, document, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Agility) -> None: + with client.knowledge_bases.sources.documents.with_streaming_response.retrieve( + document_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + document = response.parse() + assert_matches_type(Document, document, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Agility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `knowledge_base_id` but received ''"): + client.knowledge_bases.sources.documents.with_raw_response.retrieve( + document_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="", + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `source_id` but received ''"): + client.knowledge_bases.sources.documents.with_raw_response.retrieve( + document_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + source_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `document_id` but received ''"): + client.knowledge_bases.sources.documents.with_raw_response.retrieve( + document_id="", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + @parametrize + def test_method_list(self, client: Agility) -> None: + document = client.knowledge_bases.sources.documents.list( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(DocumentListResponse, document, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Agility) -> None: + document = client.knowledge_bases.sources.documents.list( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + limit=1, + offset=0, + ) + assert_matches_type(DocumentListResponse, document, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Agility) -> None: + response = client.knowledge_bases.sources.documents.with_raw_response.list( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + document = response.parse() + assert_matches_type(DocumentListResponse, document, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Agility) -> None: + with client.knowledge_bases.sources.documents.with_streaming_response.list( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + document = response.parse() + assert_matches_type(DocumentListResponse, document, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_list(self, client: Agility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `knowledge_base_id` but received ''"): + client.knowledge_bases.sources.documents.with_raw_response.list( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `source_id` but received ''"): + client.knowledge_bases.sources.documents.with_raw_response.list( + source_id="", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + +class TestAsyncDocuments: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_retrieve(self, async_client: AsyncAgility) -> None: + document = await async_client.knowledge_bases.sources.documents.retrieve( + document_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(Document, document, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncAgility) -> None: + response = await async_client.knowledge_bases.sources.documents.with_raw_response.retrieve( + document_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + document = await response.parse() + assert_matches_type(Document, document, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncAgility) -> None: + async with async_client.knowledge_bases.sources.documents.with_streaming_response.retrieve( + document_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + document = await response.parse() + assert_matches_type(Document, document, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncAgility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `knowledge_base_id` but received ''"): + await async_client.knowledge_bases.sources.documents.with_raw_response.retrieve( + document_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="", + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `source_id` but received ''"): + await async_client.knowledge_bases.sources.documents.with_raw_response.retrieve( + document_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + source_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `document_id` but received ''"): + await async_client.knowledge_bases.sources.documents.with_raw_response.retrieve( + document_id="", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncAgility) -> None: + document = await async_client.knowledge_bases.sources.documents.list( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(DocumentListResponse, document, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncAgility) -> None: + document = await async_client.knowledge_bases.sources.documents.list( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + limit=1, + offset=0, + ) + assert_matches_type(DocumentListResponse, document, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncAgility) -> None: + response = await async_client.knowledge_bases.sources.documents.with_raw_response.list( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + document = await response.parse() + assert_matches_type(DocumentListResponse, document, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncAgility) -> None: + async with async_client.knowledge_bases.sources.documents.with_streaming_response.list( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + document = await response.parse() + assert_matches_type(DocumentListResponse, document, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_list(self, async_client: AsyncAgility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `knowledge_base_id` but received ''"): + await async_client.knowledge_bases.sources.documents.with_raw_response.list( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `source_id` but received ''"): + await async_client.knowledge_bases.sources.documents.with_raw_response.list( + source_id="", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) diff --git a/tests/api_resources/knowledge_bases/test_sources.py b/tests/api_resources/knowledge_bases/test_sources.py new file mode 100644 index 0000000..d75f1b1 --- /dev/null +++ b/tests/api_resources/knowledge_bases/test_sources.py @@ -0,0 +1,896 @@ +# 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 agility import Agility, AsyncAgility +from tests.utils import assert_matches_type +from agility.types.knowledge_bases import ( + Source, + SourceListResponse, + SourceStatusResponse, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestSources: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: Agility) -> None: + source = client.knowledge_bases.sources.create( + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + description="description", + name="name", + source_params={"urls": ["string", "string", "string"]}, + source_schedule={ + "cron": "cron", + "utc_offset": 0, + }, + ) + assert_matches_type(Source, source, path=["response"]) + + @parametrize + def test_method_create_with_all_params(self, client: Agility) -> None: + source = client.knowledge_bases.sources.create( + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + description="description", + name="name", + source_params={ + "urls": ["string", "string", "string"], + "allow_backward_links": True, + "allow_external_links": True, + "exclude_regex": "exclude_regex", + "include_regex": "include_regex", + "limit": 0, + "max_depth": 0, + "name": "web_v0", + }, + source_schedule={ + "cron": "cron", + "utc_offset": 0, + }, + ) + assert_matches_type(Source, source, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Agility) -> None: + response = client.knowledge_bases.sources.with_raw_response.create( + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + description="description", + name="name", + source_params={"urls": ["string", "string", "string"]}, + source_schedule={ + "cron": "cron", + "utc_offset": 0, + }, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + source = response.parse() + assert_matches_type(Source, source, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Agility) -> None: + with client.knowledge_bases.sources.with_streaming_response.create( + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + description="description", + name="name", + source_params={"urls": ["string", "string", "string"]}, + source_schedule={ + "cron": "cron", + "utc_offset": 0, + }, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + source = response.parse() + assert_matches_type(Source, source, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_create(self, client: Agility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `knowledge_base_id` but received ''"): + client.knowledge_bases.sources.with_raw_response.create( + knowledge_base_id="", + description="description", + name="name", + source_params={"urls": ["string", "string", "string"]}, + source_schedule={ + "cron": "cron", + "utc_offset": 0, + }, + ) + + @parametrize + def test_method_retrieve(self, client: Agility) -> None: + source = client.knowledge_bases.sources.retrieve( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(Source, source, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Agility) -> None: + response = client.knowledge_bases.sources.with_raw_response.retrieve( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + source = response.parse() + assert_matches_type(Source, source, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Agility) -> None: + with client.knowledge_bases.sources.with_streaming_response.retrieve( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + source = response.parse() + assert_matches_type(Source, source, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Agility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `knowledge_base_id` but received ''"): + client.knowledge_bases.sources.with_raw_response.retrieve( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `source_id` but received ''"): + client.knowledge_bases.sources.with_raw_response.retrieve( + source_id="", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + @parametrize + def test_method_update(self, client: Agility) -> None: + source = client.knowledge_bases.sources.update( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + description="description", + name="name", + source_params={"urls": ["string", "string", "string"]}, + source_schedule={ + "cron": "cron", + "utc_offset": 0, + }, + ) + assert_matches_type(Source, source, path=["response"]) + + @parametrize + def test_method_update_with_all_params(self, client: Agility) -> None: + source = client.knowledge_bases.sources.update( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + description="description", + name="name", + source_params={ + "urls": ["string", "string", "string"], + "allow_backward_links": True, + "allow_external_links": True, + "exclude_regex": "exclude_regex", + "include_regex": "include_regex", + "limit": 0, + "max_depth": 0, + "name": "web_v0", + }, + source_schedule={ + "cron": "cron", + "utc_offset": 0, + }, + ) + assert_matches_type(Source, source, path=["response"]) + + @parametrize + def test_raw_response_update(self, client: Agility) -> None: + response = client.knowledge_bases.sources.with_raw_response.update( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + description="description", + name="name", + source_params={"urls": ["string", "string", "string"]}, + source_schedule={ + "cron": "cron", + "utc_offset": 0, + }, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + source = response.parse() + assert_matches_type(Source, source, path=["response"]) + + @parametrize + def test_streaming_response_update(self, client: Agility) -> None: + with client.knowledge_bases.sources.with_streaming_response.update( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + description="description", + name="name", + source_params={"urls": ["string", "string", "string"]}, + source_schedule={ + "cron": "cron", + "utc_offset": 0, + }, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + source = response.parse() + assert_matches_type(Source, source, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_update(self, client: Agility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `knowledge_base_id` but received ''"): + client.knowledge_bases.sources.with_raw_response.update( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="", + description="description", + name="name", + source_params={"urls": ["string", "string", "string"]}, + source_schedule={ + "cron": "cron", + "utc_offset": 0, + }, + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `source_id` but received ''"): + client.knowledge_bases.sources.with_raw_response.update( + source_id="", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + description="description", + name="name", + source_params={"urls": ["string", "string", "string"]}, + source_schedule={ + "cron": "cron", + "utc_offset": 0, + }, + ) + + @parametrize + def test_method_list(self, client: Agility) -> None: + source = client.knowledge_bases.sources.list( + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(SourceListResponse, source, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Agility) -> None: + source = client.knowledge_bases.sources.list( + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + limit=1, + offset=0, + ) + assert_matches_type(SourceListResponse, source, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Agility) -> None: + response = client.knowledge_bases.sources.with_raw_response.list( + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + source = response.parse() + assert_matches_type(SourceListResponse, source, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Agility) -> None: + with client.knowledge_bases.sources.with_streaming_response.list( + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + source = response.parse() + assert_matches_type(SourceListResponse, source, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_list(self, client: Agility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `knowledge_base_id` but received ''"): + client.knowledge_bases.sources.with_raw_response.list( + knowledge_base_id="", + ) + + @parametrize + def test_method_delete(self, client: Agility) -> None: + source = client.knowledge_bases.sources.delete( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert source is None + + @parametrize + def test_raw_response_delete(self, client: Agility) -> None: + response = client.knowledge_bases.sources.with_raw_response.delete( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + source = response.parse() + assert source is None + + @parametrize + def test_streaming_response_delete(self, client: Agility) -> None: + with client.knowledge_bases.sources.with_streaming_response.delete( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + source = response.parse() + assert source is None + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_delete(self, client: Agility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `knowledge_base_id` but received ''"): + client.knowledge_bases.sources.with_raw_response.delete( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `source_id` but received ''"): + client.knowledge_bases.sources.with_raw_response.delete( + source_id="", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + @parametrize + def test_method_status(self, client: Agility) -> None: + source = client.knowledge_bases.sources.status( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(SourceStatusResponse, source, path=["response"]) + + @parametrize + def test_raw_response_status(self, client: Agility) -> None: + response = client.knowledge_bases.sources.with_raw_response.status( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + source = response.parse() + assert_matches_type(SourceStatusResponse, source, path=["response"]) + + @parametrize + def test_streaming_response_status(self, client: Agility) -> None: + with client.knowledge_bases.sources.with_streaming_response.status( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + source = response.parse() + assert_matches_type(SourceStatusResponse, source, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_status(self, client: Agility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `knowledge_base_id` but received ''"): + client.knowledge_bases.sources.with_raw_response.status( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `source_id` but received ''"): + client.knowledge_bases.sources.with_raw_response.status( + source_id="", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + @parametrize + def test_method_sync(self, client: Agility) -> None: + source = client.knowledge_bases.sources.sync( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(object, source, path=["response"]) + + @parametrize + def test_raw_response_sync(self, client: Agility) -> None: + response = client.knowledge_bases.sources.with_raw_response.sync( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + source = response.parse() + assert_matches_type(object, source, path=["response"]) + + @parametrize + def test_streaming_response_sync(self, client: Agility) -> None: + with client.knowledge_bases.sources.with_streaming_response.sync( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + source = response.parse() + assert_matches_type(object, source, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_sync(self, client: Agility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `knowledge_base_id` but received ''"): + client.knowledge_bases.sources.with_raw_response.sync( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `source_id` but received ''"): + client.knowledge_bases.sources.with_raw_response.sync( + source_id="", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + +class TestAsyncSources: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_create(self, async_client: AsyncAgility) -> None: + source = await async_client.knowledge_bases.sources.create( + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + description="description", + name="name", + source_params={"urls": ["string", "string", "string"]}, + source_schedule={ + "cron": "cron", + "utc_offset": 0, + }, + ) + assert_matches_type(Source, source, path=["response"]) + + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncAgility) -> None: + source = await async_client.knowledge_bases.sources.create( + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + description="description", + name="name", + source_params={ + "urls": ["string", "string", "string"], + "allow_backward_links": True, + "allow_external_links": True, + "exclude_regex": "exclude_regex", + "include_regex": "include_regex", + "limit": 0, + "max_depth": 0, + "name": "web_v0", + }, + source_schedule={ + "cron": "cron", + "utc_offset": 0, + }, + ) + assert_matches_type(Source, source, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncAgility) -> None: + response = await async_client.knowledge_bases.sources.with_raw_response.create( + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + description="description", + name="name", + source_params={"urls": ["string", "string", "string"]}, + source_schedule={ + "cron": "cron", + "utc_offset": 0, + }, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + source = await response.parse() + assert_matches_type(Source, source, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncAgility) -> None: + async with async_client.knowledge_bases.sources.with_streaming_response.create( + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + description="description", + name="name", + source_params={"urls": ["string", "string", "string"]}, + source_schedule={ + "cron": "cron", + "utc_offset": 0, + }, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + source = await response.parse() + assert_matches_type(Source, source, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_create(self, async_client: AsyncAgility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `knowledge_base_id` but received ''"): + await async_client.knowledge_bases.sources.with_raw_response.create( + knowledge_base_id="", + description="description", + name="name", + source_params={"urls": ["string", "string", "string"]}, + source_schedule={ + "cron": "cron", + "utc_offset": 0, + }, + ) + + @parametrize + async def test_method_retrieve(self, async_client: AsyncAgility) -> None: + source = await async_client.knowledge_bases.sources.retrieve( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(Source, source, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncAgility) -> None: + response = await async_client.knowledge_bases.sources.with_raw_response.retrieve( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + source = await response.parse() + assert_matches_type(Source, source, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncAgility) -> None: + async with async_client.knowledge_bases.sources.with_streaming_response.retrieve( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + source = await response.parse() + assert_matches_type(Source, source, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncAgility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `knowledge_base_id` but received ''"): + await async_client.knowledge_bases.sources.with_raw_response.retrieve( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `source_id` but received ''"): + await async_client.knowledge_bases.sources.with_raw_response.retrieve( + source_id="", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + @parametrize + async def test_method_update(self, async_client: AsyncAgility) -> None: + source = await async_client.knowledge_bases.sources.update( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + description="description", + name="name", + source_params={"urls": ["string", "string", "string"]}, + source_schedule={ + "cron": "cron", + "utc_offset": 0, + }, + ) + assert_matches_type(Source, source, path=["response"]) + + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncAgility) -> None: + source = await async_client.knowledge_bases.sources.update( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + description="description", + name="name", + source_params={ + "urls": ["string", "string", "string"], + "allow_backward_links": True, + "allow_external_links": True, + "exclude_regex": "exclude_regex", + "include_regex": "include_regex", + "limit": 0, + "max_depth": 0, + "name": "web_v0", + }, + source_schedule={ + "cron": "cron", + "utc_offset": 0, + }, + ) + assert_matches_type(Source, source, path=["response"]) + + @parametrize + async def test_raw_response_update(self, async_client: AsyncAgility) -> None: + response = await async_client.knowledge_bases.sources.with_raw_response.update( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + description="description", + name="name", + source_params={"urls": ["string", "string", "string"]}, + source_schedule={ + "cron": "cron", + "utc_offset": 0, + }, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + source = await response.parse() + assert_matches_type(Source, source, path=["response"]) + + @parametrize + async def test_streaming_response_update(self, async_client: AsyncAgility) -> None: + async with async_client.knowledge_bases.sources.with_streaming_response.update( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + description="description", + name="name", + source_params={"urls": ["string", "string", "string"]}, + source_schedule={ + "cron": "cron", + "utc_offset": 0, + }, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + source = await response.parse() + assert_matches_type(Source, source, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_update(self, async_client: AsyncAgility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `knowledge_base_id` but received ''"): + await async_client.knowledge_bases.sources.with_raw_response.update( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="", + description="description", + name="name", + source_params={"urls": ["string", "string", "string"]}, + source_schedule={ + "cron": "cron", + "utc_offset": 0, + }, + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `source_id` but received ''"): + await async_client.knowledge_bases.sources.with_raw_response.update( + source_id="", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + description="description", + name="name", + source_params={"urls": ["string", "string", "string"]}, + source_schedule={ + "cron": "cron", + "utc_offset": 0, + }, + ) + + @parametrize + async def test_method_list(self, async_client: AsyncAgility) -> None: + source = await async_client.knowledge_bases.sources.list( + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(SourceListResponse, source, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncAgility) -> None: + source = await async_client.knowledge_bases.sources.list( + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + limit=1, + offset=0, + ) + assert_matches_type(SourceListResponse, source, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncAgility) -> None: + response = await async_client.knowledge_bases.sources.with_raw_response.list( + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + source = await response.parse() + assert_matches_type(SourceListResponse, source, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncAgility) -> None: + async with async_client.knowledge_bases.sources.with_streaming_response.list( + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + source = await response.parse() + assert_matches_type(SourceListResponse, source, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_list(self, async_client: AsyncAgility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `knowledge_base_id` but received ''"): + await async_client.knowledge_bases.sources.with_raw_response.list( + knowledge_base_id="", + ) + + @parametrize + async def test_method_delete(self, async_client: AsyncAgility) -> None: + source = await async_client.knowledge_bases.sources.delete( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert source is None + + @parametrize + async def test_raw_response_delete(self, async_client: AsyncAgility) -> None: + response = await async_client.knowledge_bases.sources.with_raw_response.delete( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + source = await response.parse() + assert source is None + + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncAgility) -> None: + async with async_client.knowledge_bases.sources.with_streaming_response.delete( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + source = await response.parse() + assert source is None + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_delete(self, async_client: AsyncAgility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `knowledge_base_id` but received ''"): + await async_client.knowledge_bases.sources.with_raw_response.delete( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `source_id` but received ''"): + await async_client.knowledge_bases.sources.with_raw_response.delete( + source_id="", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + @parametrize + async def test_method_status(self, async_client: AsyncAgility) -> None: + source = await async_client.knowledge_bases.sources.status( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(SourceStatusResponse, source, path=["response"]) + + @parametrize + async def test_raw_response_status(self, async_client: AsyncAgility) -> None: + response = await async_client.knowledge_bases.sources.with_raw_response.status( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + source = await response.parse() + assert_matches_type(SourceStatusResponse, source, path=["response"]) + + @parametrize + async def test_streaming_response_status(self, async_client: AsyncAgility) -> None: + async with async_client.knowledge_bases.sources.with_streaming_response.status( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + source = await response.parse() + assert_matches_type(SourceStatusResponse, source, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_status(self, async_client: AsyncAgility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `knowledge_base_id` but received ''"): + await async_client.knowledge_bases.sources.with_raw_response.status( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `source_id` but received ''"): + await async_client.knowledge_bases.sources.with_raw_response.status( + source_id="", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + @parametrize + async def test_method_sync(self, async_client: AsyncAgility) -> None: + source = await async_client.knowledge_bases.sources.sync( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(object, source, path=["response"]) + + @parametrize + async def test_raw_response_sync(self, async_client: AsyncAgility) -> None: + response = await async_client.knowledge_bases.sources.with_raw_response.sync( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + source = await response.parse() + assert_matches_type(object, source, path=["response"]) + + @parametrize + async def test_streaming_response_sync(self, async_client: AsyncAgility) -> None: + async with async_client.knowledge_bases.sources.with_streaming_response.sync( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + source = await response.parse() + assert_matches_type(object, source, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_sync(self, async_client: AsyncAgility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `knowledge_base_id` but received ''"): + await async_client.knowledge_bases.sources.with_raw_response.sync( + source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + knowledge_base_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `source_id` but received ''"): + await async_client.knowledge_bases.sources.with_raw_response.sync( + source_id="", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) diff --git a/tests/api_resources/test_assistants.py b/tests/api_resources/test_assistants.py new file mode 100644 index 0000000..5258741 --- /dev/null +++ b/tests/api_resources/test_assistants.py @@ -0,0 +1,474 @@ +# 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 agility import Agility, AsyncAgility +from tests.utils import assert_matches_type +from agility.types import ( + Assistant, + AssistantWithConfig, + AssistantListResponse, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestAssistants: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: Agility) -> None: + assistant = client.assistants.create( + description="description", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + ) + assert_matches_type(Assistant, assistant, path=["response"]) + + @parametrize + def test_method_create_with_all_params(self, client: Agility) -> None: + assistant = client.assistants.create( + description="description", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + instructions="instructions", + model="gpt-4o", + ) + assert_matches_type(Assistant, assistant, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Agility) -> None: + response = client.assistants.with_raw_response.create( + description="description", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + assistant = response.parse() + assert_matches_type(Assistant, assistant, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Agility) -> None: + with client.assistants.with_streaming_response.create( + description="description", + knowledge_base_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" + + assistant = response.parse() + assert_matches_type(Assistant, assistant, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_retrieve(self, client: Agility) -> None: + assistant = client.assistants.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(AssistantWithConfig, assistant, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Agility) -> None: + response = client.assistants.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" + assistant = response.parse() + assert_matches_type(AssistantWithConfig, assistant, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Agility) -> None: + with client.assistants.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" + + assistant = response.parse() + assert_matches_type(AssistantWithConfig, assistant, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Agility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `assistant_id` but received ''"): + client.assistants.with_raw_response.retrieve( + "", + ) + + @parametrize + def test_method_update(self, client: Agility) -> None: + assistant = client.assistants.update( + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + description="description", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + ) + assert_matches_type(AssistantWithConfig, assistant, path=["response"]) + + @parametrize + def test_method_update_with_all_params(self, client: Agility) -> None: + assistant = client.assistants.update( + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + description="description", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + instructions="instructions", + model="gpt-4o", + ) + assert_matches_type(AssistantWithConfig, assistant, path=["response"]) + + @parametrize + def test_raw_response_update(self, client: Agility) -> None: + response = client.assistants.with_raw_response.update( + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + description="description", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + assistant = response.parse() + assert_matches_type(AssistantWithConfig, assistant, path=["response"]) + + @parametrize + def test_streaming_response_update(self, client: Agility) -> None: + with client.assistants.with_streaming_response.update( + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + description="description", + knowledge_base_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" + + assistant = response.parse() + assert_matches_type(AssistantWithConfig, assistant, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_update(self, client: Agility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `assistant_id` but received ''"): + client.assistants.with_raw_response.update( + assistant_id="", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + description="description", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + ) + + @parametrize + def test_method_list(self, client: Agility) -> None: + assistant = client.assistants.list() + assert_matches_type(AssistantListResponse, assistant, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Agility) -> None: + assistant = client.assistants.list( + limit=1, + offset=0, + ) + assert_matches_type(AssistantListResponse, assistant, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Agility) -> None: + response = client.assistants.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + assistant = response.parse() + assert_matches_type(AssistantListResponse, assistant, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Agility) -> None: + with client.assistants.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + assistant = response.parse() + assert_matches_type(AssistantListResponse, assistant, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_delete(self, client: Agility) -> None: + assistant = client.assistants.delete( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert assistant is None + + @parametrize + def test_raw_response_delete(self, client: Agility) -> None: + response = client.assistants.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" + assistant = response.parse() + assert assistant is None + + @parametrize + def test_streaming_response_delete(self, client: Agility) -> None: + with client.assistants.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" + + assistant = response.parse() + assert assistant is None + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_delete(self, client: Agility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `assistant_id` but received ''"): + client.assistants.with_raw_response.delete( + "", + ) + + +class TestAsyncAssistants: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_create(self, async_client: AsyncAgility) -> None: + assistant = await async_client.assistants.create( + description="description", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + ) + assert_matches_type(Assistant, assistant, path=["response"]) + + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncAgility) -> None: + assistant = await async_client.assistants.create( + description="description", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + instructions="instructions", + model="gpt-4o", + ) + assert_matches_type(Assistant, assistant, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncAgility) -> None: + response = await async_client.assistants.with_raw_response.create( + description="description", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + assistant = await response.parse() + assert_matches_type(Assistant, assistant, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncAgility) -> None: + async with async_client.assistants.with_streaming_response.create( + description="description", + knowledge_base_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" + + assistant = await response.parse() + assert_matches_type(Assistant, assistant, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_retrieve(self, async_client: AsyncAgility) -> None: + assistant = await async_client.assistants.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(AssistantWithConfig, assistant, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncAgility) -> None: + response = await async_client.assistants.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" + assistant = await response.parse() + assert_matches_type(AssistantWithConfig, assistant, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncAgility) -> None: + async with async_client.assistants.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" + + assistant = await response.parse() + assert_matches_type(AssistantWithConfig, assistant, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncAgility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `assistant_id` but received ''"): + await async_client.assistants.with_raw_response.retrieve( + "", + ) + + @parametrize + async def test_method_update(self, async_client: AsyncAgility) -> None: + assistant = await async_client.assistants.update( + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + description="description", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + ) + assert_matches_type(AssistantWithConfig, assistant, path=["response"]) + + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncAgility) -> None: + assistant = await async_client.assistants.update( + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + description="description", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + instructions="instructions", + model="gpt-4o", + ) + assert_matches_type(AssistantWithConfig, assistant, path=["response"]) + + @parametrize + async def test_raw_response_update(self, async_client: AsyncAgility) -> None: + response = await async_client.assistants.with_raw_response.update( + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + description="description", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + assistant = await response.parse() + assert_matches_type(AssistantWithConfig, assistant, path=["response"]) + + @parametrize + async def test_streaming_response_update(self, async_client: AsyncAgility) -> None: + async with async_client.assistants.with_streaming_response.update( + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + description="description", + knowledge_base_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" + + assistant = await response.parse() + assert_matches_type(AssistantWithConfig, assistant, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_update(self, async_client: AsyncAgility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `assistant_id` but received ''"): + await async_client.assistants.with_raw_response.update( + assistant_id="", + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + description="description", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncAgility) -> None: + assistant = await async_client.assistants.list() + assert_matches_type(AssistantListResponse, assistant, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncAgility) -> None: + assistant = await async_client.assistants.list( + limit=1, + offset=0, + ) + assert_matches_type(AssistantListResponse, assistant, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncAgility) -> None: + response = await async_client.assistants.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + assistant = await response.parse() + assert_matches_type(AssistantListResponse, assistant, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncAgility) -> None: + async with async_client.assistants.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + assistant = await response.parse() + assert_matches_type(AssistantListResponse, assistant, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_delete(self, async_client: AsyncAgility) -> None: + assistant = await async_client.assistants.delete( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert assistant is None + + @parametrize + async def test_raw_response_delete(self, async_client: AsyncAgility) -> None: + response = await async_client.assistants.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" + assistant = await response.parse() + assert assistant is None + + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncAgility) -> None: + async with async_client.assistants.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" + + assistant = await response.parse() + assert assistant is None + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_delete(self, async_client: AsyncAgility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `assistant_id` but received ''"): + await async_client.assistants.with_raw_response.delete( + "", + ) diff --git a/tests/api_resources/test_health.py b/tests/api_resources/test_health.py new file mode 100644 index 0000000..c448a14 --- /dev/null +++ b/tests/api_resources/test_health.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 agility import Agility, AsyncAgility +from tests.utils import assert_matches_type +from agility.types import HealthCheckResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestHealth: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_check(self, client: Agility) -> None: + health = client.health.check() + assert_matches_type(HealthCheckResponse, health, path=["response"]) + + @parametrize + def test_raw_response_check(self, client: Agility) -> None: + response = client.health.with_raw_response.check() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + health = response.parse() + assert_matches_type(HealthCheckResponse, health, path=["response"]) + + @parametrize + def test_streaming_response_check(self, client: Agility) -> None: + with client.health.with_streaming_response.check() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + health = response.parse() + assert_matches_type(HealthCheckResponse, health, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncHealth: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_check(self, async_client: AsyncAgility) -> None: + health = await async_client.health.check() + assert_matches_type(HealthCheckResponse, health, path=["response"]) + + @parametrize + async def test_raw_response_check(self, async_client: AsyncAgility) -> None: + response = await async_client.health.with_raw_response.check() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + health = await response.parse() + assert_matches_type(HealthCheckResponse, health, path=["response"]) + + @parametrize + async def test_streaming_response_check(self, async_client: AsyncAgility) -> None: + async with async_client.health.with_streaming_response.check() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + health = await response.parse() + assert_matches_type(HealthCheckResponse, health, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_knowledge_bases.py b/tests/api_resources/test_knowledge_bases.py new file mode 100644 index 0000000..9cd2110 --- /dev/null +++ b/tests/api_resources/test_knowledge_bases.py @@ -0,0 +1,487 @@ +# 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 agility import Agility, AsyncAgility +from tests.utils import assert_matches_type +from agility.types import ( + KnowledgeBaseWithConfig, + KnowledgeBaseListResponse, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestKnowledgeBases: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: Agility) -> None: + knowledge_base = client.knowledge_bases.create( + description="description", + ingestion_pipeline_params={ + "curate": {}, + "curate_document_store": {}, + "transform": {}, + "vector_store": {"weaviate_collection_name": "weaviate_collection_name"}, + }, + name="name", + ) + assert_matches_type(KnowledgeBaseWithConfig, knowledge_base, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Agility) -> None: + response = client.knowledge_bases.with_raw_response.create( + description="description", + ingestion_pipeline_params={ + "curate": {}, + "curate_document_store": {}, + "transform": {}, + "vector_store": {"weaviate_collection_name": "weaviate_collection_name"}, + }, + name="name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + knowledge_base = response.parse() + assert_matches_type(KnowledgeBaseWithConfig, knowledge_base, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Agility) -> None: + with client.knowledge_bases.with_streaming_response.create( + description="description", + ingestion_pipeline_params={ + "curate": {}, + "curate_document_store": {}, + "transform": {}, + "vector_store": {"weaviate_collection_name": "weaviate_collection_name"}, + }, + name="name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + knowledge_base = response.parse() + assert_matches_type(KnowledgeBaseWithConfig, knowledge_base, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_retrieve(self, client: Agility) -> None: + knowledge_base = client.knowledge_bases.retrieve( + "knowledge_base_id", + ) + assert_matches_type(KnowledgeBaseWithConfig, knowledge_base, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Agility) -> None: + response = client.knowledge_bases.with_raw_response.retrieve( + "knowledge_base_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + knowledge_base = response.parse() + assert_matches_type(KnowledgeBaseWithConfig, knowledge_base, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Agility) -> None: + with client.knowledge_bases.with_streaming_response.retrieve( + "knowledge_base_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + knowledge_base = response.parse() + assert_matches_type(KnowledgeBaseWithConfig, knowledge_base, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Agility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `knowledge_base_id` but received ''"): + client.knowledge_bases.with_raw_response.retrieve( + "", + ) + + @parametrize + def test_method_update(self, client: Agility) -> None: + knowledge_base = client.knowledge_bases.update( + knowledge_base_id="knowledge_base_id", + description="description", + ingestion_pipeline_params={ + "curate": {}, + "curate_document_store": {}, + "transform": {}, + "vector_store": {"weaviate_collection_name": "weaviate_collection_name"}, + }, + name="name", + ) + assert_matches_type(KnowledgeBaseWithConfig, knowledge_base, path=["response"]) + + @parametrize + def test_raw_response_update(self, client: Agility) -> None: + response = client.knowledge_bases.with_raw_response.update( + knowledge_base_id="knowledge_base_id", + description="description", + ingestion_pipeline_params={ + "curate": {}, + "curate_document_store": {}, + "transform": {}, + "vector_store": {"weaviate_collection_name": "weaviate_collection_name"}, + }, + name="name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + knowledge_base = response.parse() + assert_matches_type(KnowledgeBaseWithConfig, knowledge_base, path=["response"]) + + @parametrize + def test_streaming_response_update(self, client: Agility) -> None: + with client.knowledge_bases.with_streaming_response.update( + knowledge_base_id="knowledge_base_id", + description="description", + ingestion_pipeline_params={ + "curate": {}, + "curate_document_store": {}, + "transform": {}, + "vector_store": {"weaviate_collection_name": "weaviate_collection_name"}, + }, + name="name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + knowledge_base = response.parse() + assert_matches_type(KnowledgeBaseWithConfig, knowledge_base, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_update(self, client: Agility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `knowledge_base_id` but received ''"): + client.knowledge_bases.with_raw_response.update( + knowledge_base_id="", + description="description", + ingestion_pipeline_params={ + "curate": {}, + "curate_document_store": {}, + "transform": {}, + "vector_store": {"weaviate_collection_name": "weaviate_collection_name"}, + }, + name="name", + ) + + @parametrize + def test_method_list(self, client: Agility) -> None: + knowledge_base = client.knowledge_bases.list() + assert_matches_type(KnowledgeBaseListResponse, knowledge_base, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Agility) -> None: + knowledge_base = client.knowledge_bases.list( + limit=1, + offset=0, + ) + assert_matches_type(KnowledgeBaseListResponse, knowledge_base, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Agility) -> None: + response = client.knowledge_bases.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + knowledge_base = response.parse() + assert_matches_type(KnowledgeBaseListResponse, knowledge_base, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Agility) -> None: + with client.knowledge_bases.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + knowledge_base = response.parse() + assert_matches_type(KnowledgeBaseListResponse, knowledge_base, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_delete(self, client: Agility) -> None: + knowledge_base = client.knowledge_bases.delete( + "knowledge_base_id", + ) + assert knowledge_base is None + + @parametrize + def test_raw_response_delete(self, client: Agility) -> None: + response = client.knowledge_bases.with_raw_response.delete( + "knowledge_base_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + knowledge_base = response.parse() + assert knowledge_base is None + + @parametrize + def test_streaming_response_delete(self, client: Agility) -> None: + with client.knowledge_bases.with_streaming_response.delete( + "knowledge_base_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + knowledge_base = response.parse() + assert knowledge_base is None + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_delete(self, client: Agility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `knowledge_base_id` but received ''"): + client.knowledge_bases.with_raw_response.delete( + "", + ) + + +class TestAsyncKnowledgeBases: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_create(self, async_client: AsyncAgility) -> None: + knowledge_base = await async_client.knowledge_bases.create( + description="description", + ingestion_pipeline_params={ + "curate": {}, + "curate_document_store": {}, + "transform": {}, + "vector_store": {"weaviate_collection_name": "weaviate_collection_name"}, + }, + name="name", + ) + assert_matches_type(KnowledgeBaseWithConfig, knowledge_base, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncAgility) -> None: + response = await async_client.knowledge_bases.with_raw_response.create( + description="description", + ingestion_pipeline_params={ + "curate": {}, + "curate_document_store": {}, + "transform": {}, + "vector_store": {"weaviate_collection_name": "weaviate_collection_name"}, + }, + name="name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + knowledge_base = await response.parse() + assert_matches_type(KnowledgeBaseWithConfig, knowledge_base, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncAgility) -> None: + async with async_client.knowledge_bases.with_streaming_response.create( + description="description", + ingestion_pipeline_params={ + "curate": {}, + "curate_document_store": {}, + "transform": {}, + "vector_store": {"weaviate_collection_name": "weaviate_collection_name"}, + }, + name="name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + knowledge_base = await response.parse() + assert_matches_type(KnowledgeBaseWithConfig, knowledge_base, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_retrieve(self, async_client: AsyncAgility) -> None: + knowledge_base = await async_client.knowledge_bases.retrieve( + "knowledge_base_id", + ) + assert_matches_type(KnowledgeBaseWithConfig, knowledge_base, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncAgility) -> None: + response = await async_client.knowledge_bases.with_raw_response.retrieve( + "knowledge_base_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + knowledge_base = await response.parse() + assert_matches_type(KnowledgeBaseWithConfig, knowledge_base, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncAgility) -> None: + async with async_client.knowledge_bases.with_streaming_response.retrieve( + "knowledge_base_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + knowledge_base = await response.parse() + assert_matches_type(KnowledgeBaseWithConfig, knowledge_base, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncAgility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `knowledge_base_id` but received ''"): + await async_client.knowledge_bases.with_raw_response.retrieve( + "", + ) + + @parametrize + async def test_method_update(self, async_client: AsyncAgility) -> None: + knowledge_base = await async_client.knowledge_bases.update( + knowledge_base_id="knowledge_base_id", + description="description", + ingestion_pipeline_params={ + "curate": {}, + "curate_document_store": {}, + "transform": {}, + "vector_store": {"weaviate_collection_name": "weaviate_collection_name"}, + }, + name="name", + ) + assert_matches_type(KnowledgeBaseWithConfig, knowledge_base, path=["response"]) + + @parametrize + async def test_raw_response_update(self, async_client: AsyncAgility) -> None: + response = await async_client.knowledge_bases.with_raw_response.update( + knowledge_base_id="knowledge_base_id", + description="description", + ingestion_pipeline_params={ + "curate": {}, + "curate_document_store": {}, + "transform": {}, + "vector_store": {"weaviate_collection_name": "weaviate_collection_name"}, + }, + name="name", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + knowledge_base = await response.parse() + assert_matches_type(KnowledgeBaseWithConfig, knowledge_base, path=["response"]) + + @parametrize + async def test_streaming_response_update(self, async_client: AsyncAgility) -> None: + async with async_client.knowledge_bases.with_streaming_response.update( + knowledge_base_id="knowledge_base_id", + description="description", + ingestion_pipeline_params={ + "curate": {}, + "curate_document_store": {}, + "transform": {}, + "vector_store": {"weaviate_collection_name": "weaviate_collection_name"}, + }, + name="name", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + knowledge_base = await response.parse() + assert_matches_type(KnowledgeBaseWithConfig, knowledge_base, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_update(self, async_client: AsyncAgility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `knowledge_base_id` but received ''"): + await async_client.knowledge_bases.with_raw_response.update( + knowledge_base_id="", + description="description", + ingestion_pipeline_params={ + "curate": {}, + "curate_document_store": {}, + "transform": {}, + "vector_store": {"weaviate_collection_name": "weaviate_collection_name"}, + }, + name="name", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncAgility) -> None: + knowledge_base = await async_client.knowledge_bases.list() + assert_matches_type(KnowledgeBaseListResponse, knowledge_base, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncAgility) -> None: + knowledge_base = await async_client.knowledge_bases.list( + limit=1, + offset=0, + ) + assert_matches_type(KnowledgeBaseListResponse, knowledge_base, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncAgility) -> None: + response = await async_client.knowledge_bases.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + knowledge_base = await response.parse() + assert_matches_type(KnowledgeBaseListResponse, knowledge_base, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncAgility) -> None: + async with async_client.knowledge_bases.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + knowledge_base = await response.parse() + assert_matches_type(KnowledgeBaseListResponse, knowledge_base, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_delete(self, async_client: AsyncAgility) -> None: + knowledge_base = await async_client.knowledge_bases.delete( + "knowledge_base_id", + ) + assert knowledge_base is None + + @parametrize + async def test_raw_response_delete(self, async_client: AsyncAgility) -> None: + response = await async_client.knowledge_bases.with_raw_response.delete( + "knowledge_base_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + knowledge_base = await response.parse() + assert knowledge_base is None + + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncAgility) -> None: + async with async_client.knowledge_bases.with_streaming_response.delete( + "knowledge_base_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + knowledge_base = await response.parse() + assert knowledge_base is None + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_delete(self, async_client: AsyncAgility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `knowledge_base_id` but received ''"): + await async_client.knowledge_bases.with_raw_response.delete( + "", + ) diff --git a/tests/api_resources/test_threads.py b/tests/api_resources/test_threads.py new file mode 100644 index 0000000..250a6dd --- /dev/null +++ b/tests/api_resources/test_threads.py @@ -0,0 +1,290 @@ +# 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 agility import Agility, AsyncAgility +from tests.utils import assert_matches_type +from agility.types import Thread, ThreadListResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestThreads: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: Agility) -> None: + thread = client.threads.create() + assert_matches_type(Thread, thread, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Agility) -> None: + response = client.threads.with_raw_response.create() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + thread = response.parse() + assert_matches_type(Thread, thread, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Agility) -> None: + with client.threads.with_streaming_response.create() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + thread = response.parse() + assert_matches_type(Thread, thread, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_retrieve(self, client: Agility) -> None: + thread = client.threads.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(Thread, thread, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Agility) -> None: + response = client.threads.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" + thread = response.parse() + assert_matches_type(Thread, thread, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Agility) -> None: + with client.threads.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" + + thread = response.parse() + assert_matches_type(Thread, thread, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Agility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `thread_id` but received ''"): + client.threads.with_raw_response.retrieve( + "", + ) + + @parametrize + def test_method_list(self, client: Agility) -> None: + thread = client.threads.list() + assert_matches_type(ThreadListResponse, thread, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Agility) -> None: + thread = client.threads.list( + limit=1, + offset=0, + ) + assert_matches_type(ThreadListResponse, thread, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Agility) -> None: + response = client.threads.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + thread = response.parse() + assert_matches_type(ThreadListResponse, thread, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Agility) -> None: + with client.threads.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + thread = response.parse() + assert_matches_type(ThreadListResponse, thread, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_delete(self, client: Agility) -> None: + thread = client.threads.delete( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert thread is None + + @parametrize + def test_raw_response_delete(self, client: Agility) -> None: + response = client.threads.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" + thread = response.parse() + assert thread is None + + @parametrize + def test_streaming_response_delete(self, client: Agility) -> None: + with client.threads.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" + + thread = response.parse() + assert thread is None + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_delete(self, client: Agility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `thread_id` but received ''"): + client.threads.with_raw_response.delete( + "", + ) + + +class TestAsyncThreads: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_create(self, async_client: AsyncAgility) -> None: + thread = await async_client.threads.create() + assert_matches_type(Thread, thread, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncAgility) -> None: + response = await async_client.threads.with_raw_response.create() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + thread = await response.parse() + assert_matches_type(Thread, thread, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncAgility) -> None: + async with async_client.threads.with_streaming_response.create() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + thread = await response.parse() + assert_matches_type(Thread, thread, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_retrieve(self, async_client: AsyncAgility) -> None: + thread = await async_client.threads.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(Thread, thread, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncAgility) -> None: + response = await async_client.threads.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" + thread = await response.parse() + assert_matches_type(Thread, thread, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncAgility) -> None: + async with async_client.threads.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" + + thread = await response.parse() + assert_matches_type(Thread, thread, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncAgility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `thread_id` but received ''"): + await async_client.threads.with_raw_response.retrieve( + "", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncAgility) -> None: + thread = await async_client.threads.list() + assert_matches_type(ThreadListResponse, thread, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncAgility) -> None: + thread = await async_client.threads.list( + limit=1, + offset=0, + ) + assert_matches_type(ThreadListResponse, thread, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncAgility) -> None: + response = await async_client.threads.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + thread = await response.parse() + assert_matches_type(ThreadListResponse, thread, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncAgility) -> None: + async with async_client.threads.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + thread = await response.parse() + assert_matches_type(ThreadListResponse, thread, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_delete(self, async_client: AsyncAgility) -> None: + thread = await async_client.threads.delete( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert thread is None + + @parametrize + async def test_raw_response_delete(self, async_client: AsyncAgility) -> None: + response = await async_client.threads.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" + thread = await response.parse() + assert thread is None + + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncAgility) -> None: + async with async_client.threads.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" + + thread = await response.parse() + assert thread is None + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_delete(self, async_client: AsyncAgility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `thread_id` but received ''"): + await async_client.threads.with_raw_response.delete( + "", + ) diff --git a/tests/api_resources/test_users.py b/tests/api_resources/test_users.py new file mode 100644 index 0000000..bfcbc6e --- /dev/null +++ b/tests/api_resources/test_users.py @@ -0,0 +1,98 @@ +# 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 agility import Agility, AsyncAgility +from tests.utils import assert_matches_type +from agility.types import User + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestUsers: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_retrieve(self, client: Agility) -> None: + user = client.users.retrieve( + "user_id", + ) + assert_matches_type(User, user, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Agility) -> None: + response = client.users.with_raw_response.retrieve( + "user_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + user = response.parse() + assert_matches_type(User, user, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Agility) -> None: + with client.users.with_streaming_response.retrieve( + "user_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + user = response.parse() + assert_matches_type(User, user, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Agility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `user_id` but received ''"): + client.users.with_raw_response.retrieve( + "", + ) + + +class TestAsyncUsers: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_retrieve(self, async_client: AsyncAgility) -> None: + user = await async_client.users.retrieve( + "user_id", + ) + assert_matches_type(User, user, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncAgility) -> None: + response = await async_client.users.with_raw_response.retrieve( + "user_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + user = await response.parse() + assert_matches_type(User, user, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncAgility) -> None: + async with async_client.users.with_streaming_response.retrieve( + "user_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + user = await response.parse() + assert_matches_type(User, user, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncAgility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `user_id` but received ''"): + await async_client.users.with_raw_response.retrieve( + "", + ) diff --git a/tests/api_resources/threads/__init__.py b/tests/api_resources/threads/__init__.py new file mode 100644 index 0000000..fd8019a --- /dev/null +++ b/tests/api_resources/threads/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/threads/test_messages.py b/tests/api_resources/threads/test_messages.py new file mode 100644 index 0000000..3f449c9 --- /dev/null +++ b/tests/api_resources/threads/test_messages.py @@ -0,0 +1,428 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from agility import Agility, AsyncAgility +from tests.utils import assert_matches_type +from agility.types.threads import Message, MessageListResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestMessages: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: Agility) -> None: + message = client.threads.messages.create( + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + content="content", + metadata={}, + role="user", + ) + assert_matches_type(Message, message, path=["response"]) + + @parametrize + def test_method_create_with_all_params(self, client: Agility) -> None: + message = client.threads.messages.create( + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + content="content", + metadata={"trustworthiness_score": 0}, + role="user", + ) + assert_matches_type(Message, message, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Agility) -> None: + response = client.threads.messages.with_raw_response.create( + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + content="content", + metadata={}, + role="user", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + message = response.parse() + assert_matches_type(Message, message, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Agility) -> None: + with client.threads.messages.with_streaming_response.create( + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + content="content", + metadata={}, + role="user", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + message = response.parse() + assert_matches_type(Message, message, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_create(self, client: Agility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `thread_id` but received ''"): + client.threads.messages.with_raw_response.create( + thread_id="", + content="content", + metadata={}, + role="user", + ) + + @parametrize + def test_method_retrieve(self, client: Agility) -> None: + message = client.threads.messages.retrieve( + message_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(Message, message, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Agility) -> None: + response = client.threads.messages.with_raw_response.retrieve( + message_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + message = response.parse() + assert_matches_type(Message, message, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Agility) -> None: + with client.threads.messages.with_streaming_response.retrieve( + message_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + message = response.parse() + assert_matches_type(Message, message, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Agility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `thread_id` but received ''"): + client.threads.messages.with_raw_response.retrieve( + message_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `message_id` but received ''"): + client.threads.messages.with_raw_response.retrieve( + message_id="", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + @parametrize + def test_method_list(self, client: Agility) -> None: + message = client.threads.messages.list( + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(MessageListResponse, message, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Agility) -> None: + message = client.threads.messages.list( + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + limit=1, + offset=0, + ) + assert_matches_type(MessageListResponse, message, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Agility) -> None: + response = client.threads.messages.with_raw_response.list( + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + message = response.parse() + assert_matches_type(MessageListResponse, message, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Agility) -> None: + with client.threads.messages.with_streaming_response.list( + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + message = response.parse() + assert_matches_type(MessageListResponse, message, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_list(self, client: Agility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `thread_id` but received ''"): + client.threads.messages.with_raw_response.list( + thread_id="", + ) + + @parametrize + def test_method_delete(self, client: Agility) -> None: + message = client.threads.messages.delete( + message_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert message is None + + @parametrize + def test_raw_response_delete(self, client: Agility) -> None: + response = client.threads.messages.with_raw_response.delete( + message_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + message = response.parse() + assert message is None + + @parametrize + def test_streaming_response_delete(self, client: Agility) -> None: + with client.threads.messages.with_streaming_response.delete( + message_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + message = response.parse() + assert message is None + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_delete(self, client: Agility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `thread_id` but received ''"): + client.threads.messages.with_raw_response.delete( + message_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `message_id` but received ''"): + client.threads.messages.with_raw_response.delete( + message_id="", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + +class TestAsyncMessages: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_create(self, async_client: AsyncAgility) -> None: + message = await async_client.threads.messages.create( + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + content="content", + metadata={}, + role="user", + ) + assert_matches_type(Message, message, path=["response"]) + + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncAgility) -> None: + message = await async_client.threads.messages.create( + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + content="content", + metadata={"trustworthiness_score": 0}, + role="user", + ) + assert_matches_type(Message, message, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncAgility) -> None: + response = await async_client.threads.messages.with_raw_response.create( + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + content="content", + metadata={}, + role="user", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + message = await response.parse() + assert_matches_type(Message, message, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncAgility) -> None: + async with async_client.threads.messages.with_streaming_response.create( + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + content="content", + metadata={}, + role="user", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + message = await response.parse() + assert_matches_type(Message, message, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_create(self, async_client: AsyncAgility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `thread_id` but received ''"): + await async_client.threads.messages.with_raw_response.create( + thread_id="", + content="content", + metadata={}, + role="user", + ) + + @parametrize + async def test_method_retrieve(self, async_client: AsyncAgility) -> None: + message = await async_client.threads.messages.retrieve( + message_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(Message, message, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncAgility) -> None: + response = await async_client.threads.messages.with_raw_response.retrieve( + message_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + message = await response.parse() + assert_matches_type(Message, message, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncAgility) -> None: + async with async_client.threads.messages.with_streaming_response.retrieve( + message_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + message = await response.parse() + assert_matches_type(Message, message, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncAgility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `thread_id` but received ''"): + await async_client.threads.messages.with_raw_response.retrieve( + message_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `message_id` but received ''"): + await async_client.threads.messages.with_raw_response.retrieve( + message_id="", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncAgility) -> None: + message = await async_client.threads.messages.list( + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(MessageListResponse, message, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncAgility) -> None: + message = await async_client.threads.messages.list( + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + limit=1, + offset=0, + ) + assert_matches_type(MessageListResponse, message, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncAgility) -> None: + response = await async_client.threads.messages.with_raw_response.list( + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + message = await response.parse() + assert_matches_type(MessageListResponse, message, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncAgility) -> None: + async with async_client.threads.messages.with_streaming_response.list( + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + message = await response.parse() + assert_matches_type(MessageListResponse, message, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_list(self, async_client: AsyncAgility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `thread_id` but received ''"): + await async_client.threads.messages.with_raw_response.list( + thread_id="", + ) + + @parametrize + async def test_method_delete(self, async_client: AsyncAgility) -> None: + message = await async_client.threads.messages.delete( + message_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert message is None + + @parametrize + async def test_raw_response_delete(self, async_client: AsyncAgility) -> None: + response = await async_client.threads.messages.with_raw_response.delete( + message_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + message = await response.parse() + assert message is None + + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncAgility) -> None: + async with async_client.threads.messages.with_streaming_response.delete( + message_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + message = await response.parse() + assert message is None + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_delete(self, async_client: AsyncAgility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `thread_id` but received ''"): + await async_client.threads.messages.with_raw_response.delete( + message_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `message_id` but received ''"): + await async_client.threads.messages.with_raw_response.delete( + message_id="", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) diff --git a/tests/api_resources/threads/test_runs.py b/tests/api_resources/threads/test_runs.py new file mode 100644 index 0000000..d286533 --- /dev/null +++ b/tests/api_resources/threads/test_runs.py @@ -0,0 +1,510 @@ +# 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 agility import Agility, AsyncAgility +from tests.utils import assert_matches_type +from agility.types.threads import Run + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestRuns: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: Agility) -> None: + run = client.threads.runs.create( + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(Run, run, path=["response"]) + + @parametrize + def test_method_create_with_all_params(self, client: Agility) -> None: + run = client.threads.runs.create( + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + additional_instructions="additional_instructions", + additional_messages=[ + { + "content": "content", + "metadata": {"trustworthiness_score": 0}, + "role": "user", + "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + }, + { + "content": "content", + "metadata": {"trustworthiness_score": 0}, + "role": "user", + "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + }, + { + "content": "content", + "metadata": {"trustworthiness_score": 0}, + "role": "user", + "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + }, + ], + instructions="instructions", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + model="gpt-4o", + ) + assert_matches_type(Run, run, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Agility) -> None: + response = client.threads.runs.with_raw_response.create( + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + run = response.parse() + assert_matches_type(Run, run, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Agility) -> None: + with client.threads.runs.with_streaming_response.create( + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + run = response.parse() + assert_matches_type(Run, run, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_create(self, client: Agility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `thread_id` but received ''"): + client.threads.runs.with_raw_response.create( + thread_id="", + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + @parametrize + def test_method_retrieve(self, client: Agility) -> None: + run = client.threads.runs.retrieve( + run_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(Run, run, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Agility) -> None: + response = client.threads.runs.with_raw_response.retrieve( + run_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + run = response.parse() + assert_matches_type(Run, run, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Agility) -> None: + with client.threads.runs.with_streaming_response.retrieve( + run_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + run = response.parse() + assert_matches_type(Run, run, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Agility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `thread_id` but received ''"): + client.threads.runs.with_raw_response.retrieve( + run_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `run_id` but received ''"): + client.threads.runs.with_raw_response.retrieve( + run_id="", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + @parametrize + def test_method_delete(self, client: Agility) -> None: + run = client.threads.runs.delete( + run_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert run is None + + @parametrize + def test_raw_response_delete(self, client: Agility) -> None: + response = client.threads.runs.with_raw_response.delete( + run_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + run = response.parse() + assert run is None + + @parametrize + def test_streaming_response_delete(self, client: Agility) -> None: + with client.threads.runs.with_streaming_response.delete( + run_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + run = response.parse() + assert run is None + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_delete(self, client: Agility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `thread_id` but received ''"): + client.threads.runs.with_raw_response.delete( + run_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `run_id` but received ''"): + client.threads.runs.with_raw_response.delete( + run_id="", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + @parametrize + def test_method_stream(self, client: Agility) -> None: + run = client.threads.runs.stream( + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(object, run, path=["response"]) + + @parametrize + def test_method_stream_with_all_params(self, client: Agility) -> None: + run = client.threads.runs.stream( + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + additional_instructions="additional_instructions", + additional_messages=[ + { + "content": "content", + "metadata": {"trustworthiness_score": 0}, + "role": "user", + "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + }, + { + "content": "content", + "metadata": {"trustworthiness_score": 0}, + "role": "user", + "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + }, + { + "content": "content", + "metadata": {"trustworthiness_score": 0}, + "role": "user", + "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + }, + ], + instructions="instructions", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + model="gpt-4o", + ) + assert_matches_type(object, run, path=["response"]) + + @parametrize + def test_raw_response_stream(self, client: Agility) -> None: + response = client.threads.runs.with_raw_response.stream( + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + run = response.parse() + assert_matches_type(object, run, path=["response"]) + + @parametrize + def test_streaming_response_stream(self, client: Agility) -> None: + with client.threads.runs.with_streaming_response.stream( + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + run = response.parse() + assert_matches_type(object, run, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_stream(self, client: Agility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `thread_id` but received ''"): + client.threads.runs.with_raw_response.stream( + thread_id="", + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + +class TestAsyncRuns: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_create(self, async_client: AsyncAgility) -> None: + run = await async_client.threads.runs.create( + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(Run, run, path=["response"]) + + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncAgility) -> None: + run = await async_client.threads.runs.create( + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + additional_instructions="additional_instructions", + additional_messages=[ + { + "content": "content", + "metadata": {"trustworthiness_score": 0}, + "role": "user", + "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + }, + { + "content": "content", + "metadata": {"trustworthiness_score": 0}, + "role": "user", + "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + }, + { + "content": "content", + "metadata": {"trustworthiness_score": 0}, + "role": "user", + "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + }, + ], + instructions="instructions", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + model="gpt-4o", + ) + assert_matches_type(Run, run, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncAgility) -> None: + response = await async_client.threads.runs.with_raw_response.create( + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + run = await response.parse() + assert_matches_type(Run, run, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncAgility) -> None: + async with async_client.threads.runs.with_streaming_response.create( + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + run = await response.parse() + assert_matches_type(Run, run, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_create(self, async_client: AsyncAgility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `thread_id` but received ''"): + await async_client.threads.runs.with_raw_response.create( + thread_id="", + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + @parametrize + async def test_method_retrieve(self, async_client: AsyncAgility) -> None: + run = await async_client.threads.runs.retrieve( + run_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(Run, run, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncAgility) -> None: + response = await async_client.threads.runs.with_raw_response.retrieve( + run_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + run = await response.parse() + assert_matches_type(Run, run, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncAgility) -> None: + async with async_client.threads.runs.with_streaming_response.retrieve( + run_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + run = await response.parse() + assert_matches_type(Run, run, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncAgility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `thread_id` but received ''"): + await async_client.threads.runs.with_raw_response.retrieve( + run_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `run_id` but received ''"): + await async_client.threads.runs.with_raw_response.retrieve( + run_id="", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + @parametrize + async def test_method_delete(self, async_client: AsyncAgility) -> None: + run = await async_client.threads.runs.delete( + run_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert run is None + + @parametrize + async def test_raw_response_delete(self, async_client: AsyncAgility) -> None: + response = await async_client.threads.runs.with_raw_response.delete( + run_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + run = await response.parse() + assert run is None + + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncAgility) -> None: + async with async_client.threads.runs.with_streaming_response.delete( + run_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + run = await response.parse() + assert run is None + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_delete(self, async_client: AsyncAgility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `thread_id` but received ''"): + await async_client.threads.runs.with_raw_response.delete( + run_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `run_id` but received ''"): + await async_client.threads.runs.with_raw_response.delete( + run_id="", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + @parametrize + async def test_method_stream(self, async_client: AsyncAgility) -> None: + run = await async_client.threads.runs.stream( + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(object, run, path=["response"]) + + @parametrize + async def test_method_stream_with_all_params(self, async_client: AsyncAgility) -> None: + run = await async_client.threads.runs.stream( + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + additional_instructions="additional_instructions", + additional_messages=[ + { + "content": "content", + "metadata": {"trustworthiness_score": 0}, + "role": "user", + "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + }, + { + "content": "content", + "metadata": {"trustworthiness_score": 0}, + "role": "user", + "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + }, + { + "content": "content", + "metadata": {"trustworthiness_score": 0}, + "role": "user", + "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + }, + ], + instructions="instructions", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + model="gpt-4o", + ) + assert_matches_type(object, run, path=["response"]) + + @parametrize + async def test_raw_response_stream(self, async_client: AsyncAgility) -> None: + response = await async_client.threads.runs.with_raw_response.stream( + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + run = await response.parse() + assert_matches_type(object, run, path=["response"]) + + @parametrize + async def test_streaming_response_stream(self, async_client: AsyncAgility) -> None: + async with async_client.threads.runs.with_streaming_response.stream( + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + run = await response.parse() + assert_matches_type(object, run, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_stream(self, async_client: AsyncAgility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `thread_id` but received ''"): + await async_client.threads.runs.with_raw_response.stream( + thread_id="", + assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) diff --git a/tests/api_resources/users/__init__.py b/tests/api_resources/users/__init__.py new file mode 100644 index 0000000..fd8019a --- /dev/null +++ b/tests/api_resources/users/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/users/test_api_key.py b/tests/api_resources/users/test_api_key.py new file mode 100644 index 0000000..6440582 --- /dev/null +++ b/tests/api_resources/users/test_api_key.py @@ -0,0 +1,174 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from agility import Agility, AsyncAgility +from tests.utils import assert_matches_type +from agility.types import User + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestAPIKey: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_retrieve(self, client: Agility) -> None: + api_key = client.users.api_key.retrieve( + "user_id", + ) + assert_matches_type(str, api_key, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Agility) -> None: + response = client.users.api_key.with_raw_response.retrieve( + "user_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + api_key = response.parse() + assert_matches_type(str, api_key, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Agility) -> None: + with client.users.api_key.with_streaming_response.retrieve( + "user_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + api_key = response.parse() + assert_matches_type(str, api_key, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Agility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `user_id` but received ''"): + client.users.api_key.with_raw_response.retrieve( + "", + ) + + @parametrize + def test_method_refresh(self, client: Agility) -> None: + api_key = client.users.api_key.refresh( + "user_id", + ) + assert_matches_type(User, api_key, path=["response"]) + + @parametrize + def test_raw_response_refresh(self, client: Agility) -> None: + response = client.users.api_key.with_raw_response.refresh( + "user_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + api_key = response.parse() + assert_matches_type(User, api_key, path=["response"]) + + @parametrize + def test_streaming_response_refresh(self, client: Agility) -> None: + with client.users.api_key.with_streaming_response.refresh( + "user_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + api_key = response.parse() + assert_matches_type(User, api_key, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_refresh(self, client: Agility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `user_id` but received ''"): + client.users.api_key.with_raw_response.refresh( + "", + ) + + +class TestAsyncAPIKey: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_retrieve(self, async_client: AsyncAgility) -> None: + api_key = await async_client.users.api_key.retrieve( + "user_id", + ) + assert_matches_type(str, api_key, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncAgility) -> None: + response = await async_client.users.api_key.with_raw_response.retrieve( + "user_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + api_key = await response.parse() + assert_matches_type(str, api_key, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncAgility) -> None: + async with async_client.users.api_key.with_streaming_response.retrieve( + "user_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + api_key = await response.parse() + assert_matches_type(str, api_key, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncAgility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `user_id` but received ''"): + await async_client.users.api_key.with_raw_response.retrieve( + "", + ) + + @parametrize + async def test_method_refresh(self, async_client: AsyncAgility) -> None: + api_key = await async_client.users.api_key.refresh( + "user_id", + ) + assert_matches_type(User, api_key, path=["response"]) + + @parametrize + async def test_raw_response_refresh(self, async_client: AsyncAgility) -> None: + response = await async_client.users.api_key.with_raw_response.refresh( + "user_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + api_key = await response.parse() + assert_matches_type(User, api_key, path=["response"]) + + @parametrize + async def test_streaming_response_refresh(self, async_client: AsyncAgility) -> None: + async with async_client.users.api_key.with_streaming_response.refresh( + "user_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + api_key = await response.parse() + assert_matches_type(User, api_key, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_refresh(self, async_client: AsyncAgility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `user_id` but received ''"): + await async_client.users.api_key.with_raw_response.refresh( + "", + ) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..e6eb3e1 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +import os +import asyncio +import logging +from typing import TYPE_CHECKING, Iterator, AsyncIterator + +import pytest + +from agility import Agility, AsyncAgility + +if TYPE_CHECKING: + from _pytest.fixtures import FixtureRequest + +pytest.register_assert_rewrite("tests.utils") + +logging.getLogger("agility").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") + +bearer_token = "My Bearer Token" +api_key = "My API Key" +access_key = "My Access Key" + + +@pytest.fixture(scope="session") +def client(request: FixtureRequest) -> Iterator[Agility]: + strict = getattr(request, "param", True) + if not isinstance(strict, bool): + raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") + + with Agility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_key, + _strict_response_validation=strict, + ) as client: + yield client + + +@pytest.fixture(scope="session") +async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncAgility]: + strict = getattr(request, "param", True) + if not isinstance(strict, bool): + raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") + + async with AsyncAgility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_key, + _strict_response_validation=strict, + ) as client: + yield client diff --git a/tests/sample_file.txt b/tests/sample_file.txt new file mode 100644 index 0000000..af5626b --- /dev/null +++ b/tests/sample_file.txt @@ -0,0 +1 @@ +Hello, world! diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000..62f7e0a --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,1845 @@ +# 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 agility import Agility, AsyncAgility, APIResponseValidationError +from agility._types import Omit +from agility._models import BaseModel, FinalRequestOptions +from agility._constants import RAW_RESPONSE_HEADER +from agility._exceptions import APIStatusError, APITimeoutError, APIResponseValidationError +from agility._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") +bearer_token = "My Bearer Token" +api_key = "My API Key" +access_key = "My Access 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: Agility | AsyncAgility) -> int: + transport = client._client._transport + assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport) + + pool = transport._pool + return len(pool._requests) + + +class TestAgility: + client = Agility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_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(bearer_token="another My Bearer Token") + assert copied.bearer_token == "another My Bearer Token" + assert self.client.bearer_token == "My Bearer Token" + + 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" + + copied = self.client.copy(access_key="another My Access Key") + assert copied.access_key == "another My Access Key" + assert self.client.access_key == "My Access 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 = Agility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_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 = Agility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_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. + "agility/_legacy_response.py", + "agility/_response.py", + # pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason. + "agility/_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 = Agility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_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 = Agility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_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 = Agility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_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 = Agility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_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: + Agility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_key, + _strict_response_validation=True, + http_client=cast(Any, http_client), + ) + + def test_default_headers_option(self) -> None: + client = Agility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_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 = Agility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_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_default_query_option(self) -> None: + client = Agility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_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: Agility) -> 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 = Agility( + base_url="https://example.com/from_init", + bearer_token=bearer_token, + api_key=api_key, + access_key=access_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(AGILITY_BASE_URL="http://localhost:5000/from/env"): + client = Agility( + bearer_token=bearer_token, api_key=api_key, access_key=access_key, _strict_response_validation=True + ) + assert client.base_url == "http://localhost:5000/from/env/" + + @pytest.mark.parametrize( + "client", + [ + Agility( + base_url="http://localhost:5000/custom/path/", + bearer_token=bearer_token, + api_key=api_key, + access_key=access_key, + _strict_response_validation=True, + ), + Agility( + base_url="http://localhost:5000/custom/path/", + bearer_token=bearer_token, + api_key=api_key, + access_key=access_key, + _strict_response_validation=True, + http_client=httpx.Client(), + ), + ], + ids=["standard", "custom http client"], + ) + def test_base_url_trailing_slash(self, client: Agility) -> 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", + [ + Agility( + base_url="http://localhost:5000/custom/path/", + bearer_token=bearer_token, + api_key=api_key, + access_key=access_key, + _strict_response_validation=True, + ), + Agility( + base_url="http://localhost:5000/custom/path/", + bearer_token=bearer_token, + api_key=api_key, + access_key=access_key, + _strict_response_validation=True, + http_client=httpx.Client(), + ), + ], + ids=["standard", "custom http client"], + ) + def test_base_url_no_trailing_slash(self, client: Agility) -> 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", + [ + Agility( + base_url="http://localhost:5000/custom/path/", + bearer_token=bearer_token, + api_key=api_key, + access_key=access_key, + _strict_response_validation=True, + ), + Agility( + base_url="http://localhost:5000/custom/path/", + bearer_token=bearer_token, + api_key=api_key, + access_key=access_key, + _strict_response_validation=True, + http_client=httpx.Client(), + ), + ], + ids=["standard", "custom http client"], + ) + def test_absolute_request_url(self, client: Agility) -> 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 = Agility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_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 = Agility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_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"): + Agility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_key, + _strict_response_validation=True, + max_retries=cast(Any, None), + ) + + @pytest.mark.respx(base_url=base_url) + def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None: + class Model(BaseModel): + name: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) + + strict_client = Agility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_key, + _strict_response_validation=True, + ) + + with pytest.raises(APIResponseValidationError): + strict_client.get("/foo", cast_to=Model) + + client = Agility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_key, + _strict_response_validation=False, + ) + + response = client.get("/foo", cast_to=Model) + assert isinstance(response, str) # type: ignore[unreachable] + + @pytest.mark.parametrize( + "remaining_retries,retry_after,timeout", + [ + [3, "20", 20], + [3, "0", 0.5], + [3, "-10", 0.5], + [3, "60", 60], + [3, "61", 0.5], + [3, "Fri, 29 Sep 2023 16:26:57 GMT", 20], + [3, "Fri, 29 Sep 2023 16:26:37 GMT", 0.5], + [3, "Fri, 29 Sep 2023 16:26:27 GMT", 0.5], + [3, "Fri, 29 Sep 2023 16:27:37 GMT", 60], + [3, "Fri, 29 Sep 2023 16:27:38 GMT", 0.5], + [3, "99999999999999999999999999999999999", 0.5], + [3, "Zun, 29 Sep 2023 16:26:27 GMT", 0.5], + [3, "", 0.5], + [2, "", 0.5 * 2.0], + [1, "", 0.5 * 4.0], + [-1100, "", 7.8], # test large number potentially overflowing + ], + ) + @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) + def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None: + client = Agility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_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("agility._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("/api/assistants/").mock(side_effect=httpx.TimeoutException("Test timeout error")) + + with pytest.raises(APITimeoutError): + self.client.post( + "/api/assistants/", + body=cast( + object, + dict( + description="description", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", name="name" + ), + ), + cast_to=httpx.Response, + options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, + ) + + assert _get_open_connections(self.client) == 0 + + @mock.patch("agility._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("/api/assistants/").mock(return_value=httpx.Response(500)) + + with pytest.raises(APIStatusError): + self.client.post( + "/api/assistants/", + body=cast( + object, + dict( + description="description", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", name="name" + ), + ), + cast_to=httpx.Response, + options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, + ) + + assert _get_open_connections(self.client) == 0 + + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("agility._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + def test_retries_taken(self, client: Agility, 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("/api/assistants/").mock(side_effect=retry_handler) + + response = client.assistants.with_raw_response.create( + description="description", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", name="name" + ) + + 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("agility._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + def test_omit_retry_count_header( + self, client: Agility, 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("/api/assistants/").mock(side_effect=retry_handler) + + response = client.assistants.with_raw_response.create( + description="description", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + 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("agility._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + def test_overwrite_retry_count_header( + self, client: Agility, 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("/api/assistants/").mock(side_effect=retry_handler) + + response = client.assistants.with_raw_response.create( + description="description", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + extra_headers={"x-stainless-retry-count": "42"}, + ) + + assert response.http_request.headers.get("x-stainless-retry-count") == "42" + + +class TestAsyncAgility: + client = AsyncAgility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_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(bearer_token="another My Bearer Token") + assert copied.bearer_token == "another My Bearer Token" + assert self.client.bearer_token == "My Bearer Token" + + 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" + + copied = self.client.copy(access_key="another My Access Key") + assert copied.access_key == "another My Access Key" + assert self.client.access_key == "My Access 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 = AsyncAgility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_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 = AsyncAgility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_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. + "agility/_legacy_response.py", + "agility/_response.py", + # pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason. + "agility/_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 = AsyncAgility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_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 = AsyncAgility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_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 = AsyncAgility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_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 = AsyncAgility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_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: + AsyncAgility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_key, + _strict_response_validation=True, + http_client=cast(Any, http_client), + ) + + def test_default_headers_option(self) -> None: + client = AsyncAgility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_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 = AsyncAgility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_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_default_query_option(self) -> None: + client = AsyncAgility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_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: AsyncAgility) -> 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 = AsyncAgility( + base_url="https://example.com/from_init", + bearer_token=bearer_token, + api_key=api_key, + access_key=access_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(AGILITY_BASE_URL="http://localhost:5000/from/env"): + client = AsyncAgility( + bearer_token=bearer_token, api_key=api_key, access_key=access_key, _strict_response_validation=True + ) + assert client.base_url == "http://localhost:5000/from/env/" + + @pytest.mark.parametrize( + "client", + [ + AsyncAgility( + base_url="http://localhost:5000/custom/path/", + bearer_token=bearer_token, + api_key=api_key, + access_key=access_key, + _strict_response_validation=True, + ), + AsyncAgility( + base_url="http://localhost:5000/custom/path/", + bearer_token=bearer_token, + api_key=api_key, + access_key=access_key, + _strict_response_validation=True, + http_client=httpx.AsyncClient(), + ), + ], + ids=["standard", "custom http client"], + ) + def test_base_url_trailing_slash(self, client: AsyncAgility) -> 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", + [ + AsyncAgility( + base_url="http://localhost:5000/custom/path/", + bearer_token=bearer_token, + api_key=api_key, + access_key=access_key, + _strict_response_validation=True, + ), + AsyncAgility( + base_url="http://localhost:5000/custom/path/", + bearer_token=bearer_token, + api_key=api_key, + access_key=access_key, + _strict_response_validation=True, + http_client=httpx.AsyncClient(), + ), + ], + ids=["standard", "custom http client"], + ) + def test_base_url_no_trailing_slash(self, client: AsyncAgility) -> 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", + [ + AsyncAgility( + base_url="http://localhost:5000/custom/path/", + bearer_token=bearer_token, + api_key=api_key, + access_key=access_key, + _strict_response_validation=True, + ), + AsyncAgility( + base_url="http://localhost:5000/custom/path/", + bearer_token=bearer_token, + api_key=api_key, + access_key=access_key, + _strict_response_validation=True, + http_client=httpx.AsyncClient(), + ), + ], + ids=["standard", "custom http client"], + ) + def test_absolute_request_url(self, client: AsyncAgility) -> 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 = AsyncAgility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_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 = AsyncAgility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_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"): + AsyncAgility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_key, + _strict_response_validation=True, + max_retries=cast(Any, None), + ) + + @pytest.mark.respx(base_url=base_url) + @pytest.mark.asyncio + async def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None: + class Model(BaseModel): + name: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) + + strict_client = AsyncAgility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_key, + _strict_response_validation=True, + ) + + with pytest.raises(APIResponseValidationError): + await strict_client.get("/foo", cast_to=Model) + + client = AsyncAgility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_key, + _strict_response_validation=False, + ) + + response = await client.get("/foo", cast_to=Model) + assert isinstance(response, str) # type: ignore[unreachable] + + @pytest.mark.parametrize( + "remaining_retries,retry_after,timeout", + [ + [3, "20", 20], + [3, "0", 0.5], + [3, "-10", 0.5], + [3, "60", 60], + [3, "61", 0.5], + [3, "Fri, 29 Sep 2023 16:26:57 GMT", 20], + [3, "Fri, 29 Sep 2023 16:26:37 GMT", 0.5], + [3, "Fri, 29 Sep 2023 16:26:27 GMT", 0.5], + [3, "Fri, 29 Sep 2023 16:27:37 GMT", 60], + [3, "Fri, 29 Sep 2023 16:27:38 GMT", 0.5], + [3, "99999999999999999999999999999999999", 0.5], + [3, "Zun, 29 Sep 2023 16:26:27 GMT", 0.5], + [3, "", 0.5], + [2, "", 0.5 * 2.0], + [1, "", 0.5 * 4.0], + [-1100, "", 7.8], # test large number potentially overflowing + ], + ) + @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) + @pytest.mark.asyncio + async def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None: + client = AsyncAgility( + base_url=base_url, + bearer_token=bearer_token, + api_key=api_key, + access_key=access_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("agility._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("/api/assistants/").mock(side_effect=httpx.TimeoutException("Test timeout error")) + + with pytest.raises(APITimeoutError): + await self.client.post( + "/api/assistants/", + body=cast( + object, + dict( + description="description", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", name="name" + ), + ), + cast_to=httpx.Response, + options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, + ) + + assert _get_open_connections(self.client) == 0 + + @mock.patch("agility._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("/api/assistants/").mock(return_value=httpx.Response(500)) + + with pytest.raises(APIStatusError): + await self.client.post( + "/api/assistants/", + body=cast( + object, + dict( + description="description", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", name="name" + ), + ), + cast_to=httpx.Response, + options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, + ) + + assert _get_open_connections(self.client) == 0 + + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("agility._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: AsyncAgility, 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("/api/assistants/").mock(side_effect=retry_handler) + + response = await client.assistants.with_raw_response.create( + description="description", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", name="name" + ) + + 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("agility._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: AsyncAgility, 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("/api/assistants/").mock(side_effect=retry_handler) + + response = await client.assistants.with_raw_response.create( + description="description", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + 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("agility._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: AsyncAgility, 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("/api/assistants/").mock(side_effect=retry_handler) + + response = await client.assistants.with_raw_response.create( + description="description", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + extra_headers={"x-stainless-retry-count": "42"}, + ) + + assert response.http_request.headers.get("x-stainless-retry-count") == "42" diff --git a/tests/test_deepcopy.py b/tests/test_deepcopy.py new file mode 100644 index 0000000..de6185f --- /dev/null +++ b/tests/test_deepcopy.py @@ -0,0 +1,58 @@ +from agility._utils import deepcopy_minimal + + +def assert_different_identities(obj1: object, obj2: object) -> None: + assert obj1 == obj2 + assert id(obj1) != id(obj2) + + +def test_simple_dict() -> None: + obj1 = {"foo": "bar"} + obj2 = deepcopy_minimal(obj1) + assert_different_identities(obj1, obj2) + + +def test_nested_dict() -> None: + obj1 = {"foo": {"bar": True}} + obj2 = deepcopy_minimal(obj1) + assert_different_identities(obj1, obj2) + assert_different_identities(obj1["foo"], obj2["foo"]) + + +def test_complex_nested_dict() -> None: + obj1 = {"foo": {"bar": [{"hello": "world"}]}} + obj2 = deepcopy_minimal(obj1) + assert_different_identities(obj1, obj2) + assert_different_identities(obj1["foo"], obj2["foo"]) + assert_different_identities(obj1["foo"]["bar"], obj2["foo"]["bar"]) + assert_different_identities(obj1["foo"]["bar"][0], obj2["foo"]["bar"][0]) + + +def test_simple_list() -> None: + obj1 = ["a", "b", "c"] + obj2 = deepcopy_minimal(obj1) + assert_different_identities(obj1, obj2) + + +def test_nested_list() -> None: + obj1 = ["a", [1, 2, 3]] + obj2 = deepcopy_minimal(obj1) + assert_different_identities(obj1, obj2) + assert_different_identities(obj1[1], obj2[1]) + + +class MyObject: ... + + +def test_ignores_other_types() -> None: + # custom classes + my_obj = MyObject() + obj1 = {"foo": my_obj} + obj2 = deepcopy_minimal(obj1) + assert_different_identities(obj1, obj2) + assert obj1["foo"] is my_obj + + # tuples + obj3 = ("a", "b") + obj4 = deepcopy_minimal(obj3) + assert obj3 is obj4 diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py new file mode 100644 index 0000000..f63b61d --- /dev/null +++ b/tests/test_extract_files.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +from typing import Sequence + +import pytest + +from agility._types import FileTypes +from agility._utils import extract_files + + +def test_removes_files_from_input() -> None: + query = {"foo": "bar"} + assert extract_files(query, paths=[]) == [] + assert query == {"foo": "bar"} + + query2 = {"foo": b"Bar", "hello": "world"} + assert extract_files(query2, paths=[["foo"]]) == [("foo", b"Bar")] + assert query2 == {"hello": "world"} + + query3 = {"foo": {"foo": {"bar": b"Bar"}}, "hello": "world"} + assert extract_files(query3, paths=[["foo", "foo", "bar"]]) == [("foo[foo][bar]", b"Bar")] + assert query3 == {"foo": {"foo": {}}, "hello": "world"} + + query4 = {"foo": {"bar": b"Bar", "baz": "foo"}, "hello": "world"} + assert extract_files(query4, paths=[["foo", "bar"]]) == [("foo[bar]", b"Bar")] + assert query4 == {"hello": "world", "foo": {"baz": "foo"}} + + +def test_multiple_files() -> None: + query = {"documents": [{"file": b"My first file"}, {"file": b"My second file"}]} + assert extract_files(query, paths=[["documents", "", "file"]]) == [ + ("documents[][file]", b"My first file"), + ("documents[][file]", b"My second file"), + ] + assert query == {"documents": [{}, {}]} + + +@pytest.mark.parametrize( + "query,paths,expected", + [ + [ + {"foo": {"bar": "baz"}}, + [["foo", "", "bar"]], + [], + ], + [ + {"foo": ["bar", "baz"]}, + [["foo", "bar"]], + [], + ], + [ + {"foo": {"bar": "baz"}}, + [["foo", "foo"]], + [], + ], + ], + ids=["dict expecting array", "array expecting dict", "unknown keys"], +) +def test_ignores_incorrect_paths( + query: dict[str, object], + paths: Sequence[Sequence[str]], + expected: list[tuple[str, FileTypes]], +) -> None: + assert extract_files(query, paths=paths) == expected diff --git a/tests/test_files.py b/tests/test_files.py new file mode 100644 index 0000000..9516560 --- /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 agility._files import to_httpx_files, async_to_httpx_files + +readme_path = Path(__file__).parent.parent.joinpath("README.md") + + +def test_pathlib_includes_file_name() -> None: + result = to_httpx_files({"file": readme_path}) + print(result) + assert result == IsDict({"file": IsTuple("README.md", IsBytes())}) + + +def test_tuple_input() -> None: + result = to_httpx_files([("file", readme_path)]) + print(result) + assert result == IsList(IsTuple("file", IsTuple("README.md", IsBytes()))) + + +@pytest.mark.asyncio +async def test_async_pathlib_includes_file_name() -> None: + result = await async_to_httpx_files({"file": readme_path}) + print(result) + assert result == IsDict({"file": IsTuple("README.md", IsBytes())}) + + +@pytest.mark.asyncio +async def test_async_supports_anyio_path() -> None: + result = await async_to_httpx_files({"file": anyio.Path(readme_path)}) + print(result) + assert result == IsDict({"file": IsTuple("README.md", IsBytes())}) + + +@pytest.mark.asyncio +async def test_async_tuple_input() -> None: + result = await async_to_httpx_files([("file", readme_path)]) + print(result) + assert result == IsList(IsTuple("file", IsTuple("README.md", IsBytes()))) + + +def test_string_not_allowed() -> None: + with pytest.raises(TypeError, match="Expected file types input to be a FileContent type or to be a tuple"): + to_httpx_files( + { + "file": "foo", # type: ignore + } + ) diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..94ffc80 --- /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 agility._utils import PropertyInfo +from agility._compat import PYDANTIC_V2, parse_obj, model_dump, model_json +from agility._models import BaseModel, construct_type + + +class BasicModel(BaseModel): + foo: str + + +@pytest.mark.parametrize("value", ["hello", 1], ids=["correct type", "mismatched"]) +def test_basic(value: object) -> None: + m = BasicModel.construct(foo=value) + assert m.foo == value + + +def test_directly_nested_model() -> None: + class NestedModel(BaseModel): + nested: BasicModel + + m = NestedModel.construct(nested={"foo": "Foo!"}) + assert m.nested.foo == "Foo!" + + # mismatched types + m = NestedModel.construct(nested="hello!") + assert cast(Any, m.nested) == "hello!" + + +def test_optional_nested_model() -> None: + class NestedModel(BaseModel): + nested: Optional[BasicModel] + + m1 = NestedModel.construct(nested=None) + assert m1.nested is None + + m2 = NestedModel.construct(nested={"foo": "bar"}) + assert m2.nested is not None + assert m2.nested.foo == "bar" + + # mismatched types + m3 = NestedModel.construct(nested={"foo"}) + assert isinstance(cast(Any, m3.nested), set) + assert cast(Any, m3.nested) == {"foo"} + + +def test_list_nested_model() -> None: + class NestedModel(BaseModel): + nested: List[BasicModel] + + m = NestedModel.construct(nested=[{"foo": "bar"}, {"foo": "2"}]) + assert m.nested is not None + assert isinstance(m.nested, list) + assert len(m.nested) == 2 + assert m.nested[0].foo == "bar" + assert m.nested[1].foo == "2" + + # mismatched types + m = NestedModel.construct(nested=True) + assert cast(Any, m.nested) is True + + m = NestedModel.construct(nested=[False]) + assert cast(Any, m.nested) == [False] + + +def test_optional_list_nested_model() -> None: + class NestedModel(BaseModel): + nested: Optional[List[BasicModel]] + + m1 = NestedModel.construct(nested=[{"foo": "bar"}, {"foo": "2"}]) + assert m1.nested is not None + assert isinstance(m1.nested, list) + assert len(m1.nested) == 2 + assert m1.nested[0].foo == "bar" + assert m1.nested[1].foo == "2" + + m2 = NestedModel.construct(nested=None) + assert m2.nested is None + + # mismatched types + m3 = NestedModel.construct(nested={1}) + assert cast(Any, m3.nested) == {1} + + m4 = NestedModel.construct(nested=[False]) + assert cast(Any, m4.nested) == [False] + + +def test_list_optional_items_nested_model() -> None: + class NestedModel(BaseModel): + nested: List[Optional[BasicModel]] + + m = NestedModel.construct(nested=[None, {"foo": "bar"}]) + assert m.nested is not None + assert isinstance(m.nested, list) + assert len(m.nested) == 2 + assert m.nested[0] is None + assert m.nested[1] is not None + assert m.nested[1].foo == "bar" + + # mismatched types + m3 = NestedModel.construct(nested="foo") + assert cast(Any, m3.nested) == "foo" + + m4 = NestedModel.construct(nested=[False]) + assert cast(Any, m4.nested) == [False] + + +def test_list_mismatched_type() -> None: + class NestedModel(BaseModel): + nested: List[str] + + m = NestedModel.construct(nested=False) + assert cast(Any, m.nested) is False + + +def test_raw_dictionary() -> None: + class NestedModel(BaseModel): + nested: Dict[str, str] + + m = NestedModel.construct(nested={"hello": "world"}) + assert m.nested == {"hello": "world"} + + # mismatched types + m = NestedModel.construct(nested=False) + assert cast(Any, m.nested) is False + + +def test_nested_dictionary_model() -> None: + class NestedModel(BaseModel): + nested: Dict[str, BasicModel] + + m = NestedModel.construct(nested={"hello": {"foo": "bar"}}) + assert isinstance(m.nested, dict) + assert m.nested["hello"].foo == "bar" + + # mismatched types + m = NestedModel.construct(nested={"hello": False}) + assert cast(Any, m.nested["hello"]) is False + + +def test_unknown_fields() -> None: + m1 = BasicModel.construct(foo="foo", unknown=1) + assert m1.foo == "foo" + assert cast(Any, m1).unknown == 1 + + m2 = BasicModel.construct(foo="foo", unknown={"foo_bar": True}) + assert m2.foo == "foo" + assert cast(Any, m2).unknown == {"foo_bar": True} + + assert model_dump(m2) == {"foo": "foo", "unknown": {"foo_bar": True}} + + +def test_strict_validation_unknown_fields() -> None: + class Model(BaseModel): + foo: str + + model = parse_obj(Model, dict(foo="hello!", user="Robert")) + assert model.foo == "hello!" + assert cast(Any, model).user == "Robert" + + assert model_dump(model) == {"foo": "hello!", "user": "Robert"} + + +def test_aliases() -> None: + class Model(BaseModel): + my_field: int = Field(alias="myField") + + m = Model.construct(myField=1) + assert m.my_field == 1 + + # mismatched types + m = Model.construct(myField={"hello": False}) + assert cast(Any, m.my_field) == {"hello": False} + + +def test_repr() -> None: + model = BasicModel(foo="bar") + assert str(model) == "BasicModel(foo='bar')" + assert repr(model) == "BasicModel(foo='bar')" + + +def test_repr_nested_model() -> None: + class Child(BaseModel): + name: str + age: int + + class Parent(BaseModel): + name: str + child: Child + + model = Parent(name="Robert", child=Child(name="Foo", age=5)) + assert str(model) == "Parent(name='Robert', child=Child(name='Foo', age=5))" + assert repr(model) == "Parent(name='Robert', child=Child(name='Foo', age=5))" + + +def test_optional_list() -> None: + class Submodel(BaseModel): + name: str + + class Model(BaseModel): + items: Optional[List[Submodel]] + + m = Model.construct(items=None) + assert m.items is None + + m = Model.construct(items=[]) + assert m.items == [] + + m = Model.construct(items=[{"name": "Robert"}]) + assert m.items is not None + assert len(m.items) == 1 + assert m.items[0].name == "Robert" + + +def test_nested_union_of_models() -> None: + class Submodel1(BaseModel): + bar: bool + + class Submodel2(BaseModel): + thing: str + + class Model(BaseModel): + foo: Union[Submodel1, Submodel2] + + m = Model.construct(foo={"thing": "hello"}) + assert isinstance(m.foo, Submodel2) + assert m.foo.thing == "hello" + + +def test_nested_union_of_mixed_types() -> None: + class Submodel1(BaseModel): + bar: bool + + class Model(BaseModel): + foo: Union[Submodel1, Literal[True], Literal["CARD_HOLDER"]] + + m = Model.construct(foo=True) + assert m.foo is True + + m = Model.construct(foo="CARD_HOLDER") + assert m.foo 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 cast(Any, m.items[1]) == 156 + + +def test_union_of_lists() -> None: + class SubModel1(BaseModel): + level: int + + class SubModel2(BaseModel): + name: str + + class Model(BaseModel): + items: Union[List[SubModel1], List[SubModel2]] + + # with one valid entry + m = Model.construct(items=[{"name": "Robert"}]) + assert len(m.items) == 1 + assert isinstance(m.items[0], SubModel2) + assert m.items[0].name == "Robert" + + # with two entries pointing to different types + m = Model.construct(items=[{"level": 1}, {"name": "Robert"}]) + assert len(m.items) == 2 + assert isinstance(m.items[0], SubModel1) + assert m.items[0].level == 1 + assert isinstance(m.items[1], SubModel1) + assert cast(Any, m.items[1]).name == "Robert" + + # with two entries pointing to *completely* different types + m = Model.construct(items=[{"level": -1}, 156]) + assert len(m.items) == 2 + assert isinstance(m.items[0], SubModel1) + assert m.items[0].level == -1 + assert cast(Any, m.items[1]) == 156 + + +def test_dict_of_union() -> None: + class SubModel1(BaseModel): + name: str + + class SubModel2(BaseModel): + foo: str + + class Model(BaseModel): + data: Dict[str, Union[SubModel1, SubModel2]] + + m = Model.construct(data={"hello": {"name": "there"}, "foo": {"foo": "bar"}}) + assert len(list(m.data.keys())) == 2 + assert isinstance(m.data["hello"], SubModel1) + assert m.data["hello"].name == "there" + assert isinstance(m.data["foo"], SubModel2) + assert m.data["foo"].foo == "bar" + + # TODO: test mismatched type + + +def test_double_nested_union() -> None: + class SubModel1(BaseModel): + name: str + + class SubModel2(BaseModel): + bar: str + + class Model(BaseModel): + data: Dict[str, List[Union[SubModel1, SubModel2]]] + + m = Model.construct(data={"foo": [{"bar": "baz"}, {"name": "Robert"}]}) + assert len(m.data["foo"]) == 2 + + entry1 = m.data["foo"][0] + assert isinstance(entry1, SubModel2) + assert entry1.bar == "baz" + + entry2 = m.data["foo"][1] + assert isinstance(entry2, SubModel1) + assert entry2.name == "Robert" + + # TODO: test mismatched type + + +def test_union_of_dict() -> None: + class SubModel1(BaseModel): + name: str + + class SubModel2(BaseModel): + foo: str + + class Model(BaseModel): + data: Union[Dict[str, SubModel1], Dict[str, SubModel2]] + + m = Model.construct(data={"hello": {"name": "there"}, "foo": {"foo": "bar"}}) + assert len(list(m.data.keys())) == 2 + assert isinstance(m.data["hello"], SubModel1) + assert m.data["hello"].name == "there" + assert isinstance(m.data["foo"], SubModel1) + assert cast(Any, m.data["foo"]).foo == "bar" + + +def test_iso8601_datetime() -> None: + class Model(BaseModel): + created_at: datetime + + expected = datetime(2019, 12, 27, 18, 11, 19, 117000, tzinfo=timezone.utc) + + if PYDANTIC_V2: + expected_json = '{"created_at":"2019-12-27T18:11:19.117000Z"}' + else: + expected_json = '{"created_at": "2019-12-27T18:11:19.117000+00:00"}' + + model = Model.construct(created_at="2019-12-27T18:11:19.117Z") + assert model.created_at == expected + assert model_json(model) == expected_json + + model = parse_obj(Model, dict(created_at="2019-12-27T18:11:19.117Z")) + assert model.created_at == expected + assert model_json(model) == expected_json + + +def test_does_not_coerce_int() -> None: + class Model(BaseModel): + bar: int + + assert Model.construct(bar=1).bar == 1 + assert Model.construct(bar=10.9).bar == 10.9 + assert Model.construct(bar="19").bar == "19" # type: ignore[comparison-overlap] + assert Model.construct(bar=False).bar is False + + +def test_int_to_float_safe_conversion() -> None: + class Model(BaseModel): + float_field: float + + m = Model.construct(float_field=10) + assert m.float_field == 10.0 + assert isinstance(m.float_field, float) + + m = Model.construct(float_field=10.12) + assert m.float_field == 10.12 + assert isinstance(m.float_field, float) + + # number too big + m = Model.construct(float_field=2**53 + 1) + assert m.float_field == 2**53 + 1 + assert isinstance(m.float_field, int) + + +def test_deprecated_alias() -> None: + class Model(BaseModel): + resource_id: str = Field(alias="model_id") + + @property + def model_id(self) -> str: + return self.resource_id + + m = Model.construct(model_id="id") + assert m.model_id == "id" + assert m.resource_id == "id" + assert m.resource_id is m.model_id + + m = parse_obj(Model, {"model_id": "id"}) + assert m.model_id == "id" + assert m.resource_id == "id" + assert m.resource_id is m.model_id + + +def test_omitted_fields() -> None: + class Model(BaseModel): + resource_id: Optional[str] = None + + m = Model.construct() + assert "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 0000000..a08ec55 --- /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 agility._qs import Querystring, stringify + + +def test_empty() -> None: + assert stringify({}) == "" + assert stringify({"a": {}}) == "" + assert stringify({"a": {"b": {"c": {}}}}) == "" + + +def test_basic() -> None: + assert stringify({"a": 1}) == "a=1" + assert stringify({"a": "b"}) == "a=b" + assert stringify({"a": True}) == "a=true" + assert stringify({"a": False}) == "a=false" + assert stringify({"a": 1.23456}) == "a=1.23456" + assert stringify({"a": None}) == "" + + +@pytest.mark.parametrize("method", ["class", "function"]) +def test_nested_dotted(method: str) -> None: + if method == "class": + serialise = Querystring(nested_format="dots").stringify + else: + serialise = partial(stringify, nested_format="dots") + + assert unquote(serialise({"a": {"b": "c"}})) == "a.b=c" + assert unquote(serialise({"a": {"b": "c", "d": "e", "f": "g"}})) == "a.b=c&a.d=e&a.f=g" + assert unquote(serialise({"a": {"b": {"c": {"d": "e"}}}})) == "a.b.c.d=e" + assert unquote(serialise({"a": {"b": True}})) == "a.b=true" + + +def test_nested_brackets() -> None: + assert unquote(stringify({"a": {"b": "c"}})) == "a[b]=c" + assert unquote(stringify({"a": {"b": "c", "d": "e", "f": "g"}})) == "a[b]=c&a[d]=e&a[f]=g" + assert unquote(stringify({"a": {"b": {"c": {"d": "e"}}}})) == "a[b][c][d]=e" + assert unquote(stringify({"a": {"b": True}})) == "a[b]=true" + + +@pytest.mark.parametrize("method", ["class", "function"]) +def test_array_comma(method: str) -> None: + if method == "class": + serialise = Querystring(array_format="comma").stringify + else: + serialise = partial(stringify, array_format="comma") + + assert unquote(serialise({"in": ["foo", "bar"]})) == "in=foo,bar" + assert unquote(serialise({"a": {"b": [True, False]}})) == "a[b]=true,false" + assert unquote(serialise({"a": {"b": [True, False, None, True]}})) == "a[b]=true,false,true" + + +def test_array_repeat() -> None: + assert unquote(stringify({"in": ["foo", "bar"]})) == "in=foo&in=bar" + assert unquote(stringify({"a": {"b": [True, False]}})) == "a[b]=true&a[b]=false" + assert unquote(stringify({"a": {"b": [True, False, None, True]}})) == "a[b]=true&a[b]=false&a[b]=true" + assert unquote(stringify({"in": ["foo", {"b": {"c": ["d", "e"]}}]})) == "in=foo&in[b][c]=d&in[b][c]=e" + + +@pytest.mark.parametrize("method", ["class", "function"]) +def test_array_brackets(method: str) -> None: + if method == "class": + serialise = Querystring(array_format="brackets").stringify + else: + serialise = partial(stringify, array_format="brackets") + + assert unquote(serialise({"in": ["foo", "bar"]})) == "in[]=foo&in[]=bar" + assert unquote(serialise({"a": {"b": [True, False]}})) == "a[b][]=true&a[b][]=false" + assert unquote(serialise({"a": {"b": [True, False, None, True]}})) == "a[b][]=true&a[b][]=false&a[b][]=true" + + +def test_unknown_array_format() -> None: + with pytest.raises(NotImplementedError, match="Unknown array_format value: foo, choose from comma, repeat"): + stringify({"a": ["foo", "bar"]}, array_format=cast(Any, "foo")) diff --git a/tests/test_required_args.py b/tests/test_required_args.py new file mode 100644 index 0000000..91107cf --- /dev/null +++ b/tests/test_required_args.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +import pytest + +from agility._utils import required_args + + +def test_too_many_positional_params() -> None: + @required_args(["a"]) + def foo(a: str | None = None) -> str | None: + return a + + with pytest.raises(TypeError, match=r"foo\(\) takes 1 argument\(s\) but 2 were given"): + foo("a", "b") # type: ignore + + +def test_positional_param() -> None: + @required_args(["a"]) + def foo(a: str | None = None) -> str | None: + return a + + assert foo("a") == "a" + assert foo(None) is None + assert foo(a="b") == "b" + + with pytest.raises(TypeError, match="Missing required argument: 'a'"): + foo() + + +def test_keyword_only_param() -> None: + @required_args(["a"]) + def foo(*, a: str | None = None) -> str | None: + return a + + assert foo(a="a") == "a" + assert foo(a=None) is None + assert foo(a="b") == "b" + + with pytest.raises(TypeError, match="Missing required argument: 'a'"): + foo() + + +def test_multiple_params() -> None: + @required_args(["a", "b", "c"]) + def foo(a: str = "", *, b: str = "", c: str = "") -> str | None: + return f"{a} {b} {c}" + + assert foo(a="a", b="b", c="c") == "a b c" + + error_message = r"Missing required arguments.*" + + with pytest.raises(TypeError, match=error_message): + foo() + + with pytest.raises(TypeError, match=error_message): + foo(a="a") + + with pytest.raises(TypeError, match=error_message): + foo(b="b") + + with pytest.raises(TypeError, match=error_message): + foo(c="c") + + with pytest.raises(TypeError, match=r"Missing required argument: 'a'"): + foo(b="a", c="c") + + with pytest.raises(TypeError, match=r"Missing required argument: 'b'"): + foo("a", c="c") + + +def test_multiple_variants() -> None: + @required_args(["a"], ["b"]) + def foo(*, a: str | None = None, b: str | None = None) -> str | None: + return a if a is not None else b + + assert foo(a="foo") == "foo" + assert foo(b="bar") == "bar" + assert foo(a=None) is None + assert foo(b=None) is None + + # TODO: this error message could probably be improved + with pytest.raises( + TypeError, + match=r"Missing required arguments; Expected either \('a'\) or \('b'\) arguments to be given", + ): + foo() + + +def test_multiple_params_multiple_variants() -> None: + @required_args(["a", "b"], ["c"]) + def foo(*, a: str | None = None, b: str | None = None, c: str | None = None) -> str | None: + if a is not None: + return a + if b is not None: + return b + return c + + error_message = r"Missing required arguments; Expected either \('a' and 'b'\) or \('c'\) arguments to be given" + + with pytest.raises(TypeError, match=error_message): + foo(a="foo") + + with pytest.raises(TypeError, match=error_message): + foo(b="bar") + + with pytest.raises(TypeError, match=error_message): + foo() + + assert foo(a=None, b="bar") == "bar" + assert foo(c=None) is None + assert foo(c="foo") == "foo" diff --git a/tests/test_response.py b/tests/test_response.py new file mode 100644 index 0000000..07e50be --- /dev/null +++ b/tests/test_response.py @@ -0,0 +1,277 @@ +import json +from typing import Any, List, Union, cast +from typing_extensions import Annotated + +import httpx +import pytest +import pydantic + +from agility import Agility, BaseModel, AsyncAgility +from agility._response import ( + APIResponse, + BaseAPIResponse, + AsyncAPIResponse, + BinaryAPIResponse, + AsyncBinaryAPIResponse, + extract_response_type, +) +from agility._streaming import Stream +from agility._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: Agility) -> 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 agility import BaseModel`", + ): + response.parse(to=PydanticModel) + + +@pytest.mark.asyncio +async def test_async_response_parse_mismatched_basemodel(async_client: AsyncAgility) -> 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 agility import BaseModel`", + ): + await response.parse(to=PydanticModel) + + +def test_response_parse_custom_stream(client: Agility) -> 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: AsyncAgility) -> 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: Agility) -> 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: AsyncAgility) -> 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: Agility) -> 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: AsyncAgility) -> None: + response = AsyncAPIResponse( + raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), + client=async_client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + obj = await response.parse( + to=cast("type[CustomModel]", Annotated[CustomModel, "random metadata"]), + ) + assert obj.foo == "hello!" + assert obj.bar == 2 + + +@pytest.mark.parametrize( + "content, expected", + [ + ("false", False), + ("true", True), + ("False", False), + ("True", True), + ("TrUe", True), + ("FalSe", False), + ], +) +def test_response_parse_bool(client: Agility, 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: AsyncAgility, content: str, expected: bool) -> None: + response = AsyncAPIResponse( + raw=httpx.Response(200, content=content), + client=client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + result = await response.parse(to=bool) + assert result is expected + + +class OtherModel(BaseModel): + a: str + + +@pytest.mark.parametrize("client", [False], indirect=True) # loose validation +def test_response_parse_expect_model_union_non_json_content(client: Agility) -> 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: AsyncAgility) -> None: + response = AsyncAPIResponse( + raw=httpx.Response(200, content=b"foo", headers={"Content-Type": "application/text"}), + client=async_client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + obj = await response.parse(to=cast(Any, Union[CustomModel, OtherModel])) + assert isinstance(obj, str) + assert obj == "foo" diff --git a/tests/test_streaming.py b/tests/test_streaming.py new file mode 100644 index 0000000..04c1610 --- /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 agility import Agility, AsyncAgility +from agility._streaming import Stream, AsyncStream, ServerSentEvent + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_basic(sync: bool, client: Agility, async_client: AsyncAgility) -> 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: Agility, async_client: AsyncAgility) -> 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: Agility, async_client: AsyncAgility) -> 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: Agility, async_client: AsyncAgility) -> 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: Agility, async_client: AsyncAgility) -> 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: Agility, async_client: AsyncAgility) -> 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: Agility, async_client: AsyncAgility) -> 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: Agility, async_client: AsyncAgility) -> 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: Agility, + async_client: AsyncAgility, +) -> 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: Agility, + async_client: AsyncAgility, +) -> 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: Agility, + async_client: AsyncAgility, +) -> Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]: + if sync: + return Stream(cast_to=object, client=client, response=httpx.Response(200, content=content))._iter_events() + + return AsyncStream( + cast_to=object, client=async_client, response=httpx.Response(200, content=to_aiter(content)) + )._iter_events() diff --git a/tests/test_transform.py b/tests/test_transform.py new file mode 100644 index 0000000..48460a0 --- /dev/null +++ b/tests/test_transform.py @@ -0,0 +1,410 @@ +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 agility._types import Base64FileInput +from agility._utils import ( + PropertyInfo, + transform as _transform, + parse_datetime, + async_transform as _async_transform, +) +from agility._compat import PYDANTIC_V2 +from agility._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 cast(Any, await transform(MyModel(foo="hi!"), Any, use_async)) == {"foo": "hi!"} + assert cast(Any, await transform(MyModel.construct(foo="hi!"), Any, use_async)) == {"foo": "hi!"} + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_empty_model(use_async: bool) -> None: + assert cast(Any, await transform(MyModel.construct(), Any, use_async)) == {} + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_unknown_field(use_async: bool) -> None: + assert cast(Any, await transform(MyModel.construct(my_untyped_field=True), Any, use_async)) == { + "my_untyped_field": True + } + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_mismatched_types(use_async: bool) -> None: + model = MyModel.construct(foo=True) + if PYDANTIC_V2: + with pytest.warns(UserWarning): + params = await transform(model, Any, use_async) + else: + params = await transform(model, Any, use_async) + assert cast(Any, params) == {"foo": True} + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_mismatched_object_type(use_async: bool) -> None: + model = MyModel.construct(foo=MyModel.construct(hello="world")) + if PYDANTIC_V2: + with pytest.warns(UserWarning): + params = await transform(model, Any, use_async) + else: + params = await transform(model, Any, use_async) + assert cast(Any, params) == {"foo": {"hello": "world"}} + + +class ModelNestedObjects(BaseModel): + nested: MyModel + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_nested_objects(use_async: bool) -> None: + model = ModelNestedObjects.construct(nested={"foo": "stainless"}) + assert isinstance(model.nested, MyModel) + assert cast(Any, await transform(model, Any, use_async)) == {"nested": {"foo": "stainless"}} + + +class ModelWithDefaultField(BaseModel): + foo: str + with_none_default: Union[str, None] = None + with_str_default: str = "foo" + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_default_field(use_async: bool) -> None: + # should be excluded when defaults are used + model = ModelWithDefaultField.construct() + assert model.with_none_default is None + assert model.with_str_default == "foo" + assert cast(Any, await transform(model, Any, use_async)) == {} + + # should be included when the default value is explicitly given + model = ModelWithDefaultField.construct(with_none_default=None, with_str_default="foo") + assert model.with_none_default is None + assert model.with_str_default == "foo" + assert cast(Any, await transform(model, Any, use_async)) == {"with_none_default": None, "with_str_default": "foo"} + + # should be included when a non-default value is explicitly given + model = ModelWithDefaultField.construct(with_none_default="bar", with_str_default="baz") + assert model.with_none_default == "bar" + assert model.with_str_default == "baz" + assert cast(Any, await transform(model, Any, use_async)) == {"with_none_default": "bar", "with_str_default": "baz"} + + +class TypedDictIterableUnion(TypedDict): + foo: Annotated[Union[Bar8, Iterable[Baz8]], PropertyInfo(alias="FOO")] + + +class Bar8(TypedDict): + foo_bar: Annotated[str, PropertyInfo(alias="fooBar")] + + +class Baz8(TypedDict): + foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")] + + +@parametrize +@pytest.mark.asyncio +async def test_iterable_of_dictionaries(use_async: bool) -> None: + assert await transform({"foo": [{"foo_baz": "bar"}]}, TypedDictIterableUnion, use_async) == { + "FOO": [{"fooBaz": "bar"}] + } + assert cast(Any, await transform({"foo": ({"foo_baz": "bar"},)}, TypedDictIterableUnion, use_async)) == { + "FOO": [{"fooBaz": "bar"}] + } + + def my_iter() -> Iterable[Baz8]: + yield {"foo_baz": "hello"} + yield {"foo_baz": "world"} + + assert await transform({"foo": my_iter()}, TypedDictIterableUnion, use_async) == { + "FOO": [{"fooBaz": "hello"}, {"fooBaz": "world"}] + } + + +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 0000000..8da4dab --- /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 agility._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 0000000..c3e4c2f --- /dev/null +++ b/tests/test_utils/test_typing.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +from typing import Generic, TypeVar, cast + +from agility._utils import extract_type_var_from_base + +_T = TypeVar("_T") +_T2 = TypeVar("_T2") +_T3 = TypeVar("_T3") + + +class BaseGeneric(Generic[_T]): ... + + +class SubclassGeneric(BaseGeneric[_T]): ... + + +class BaseGenericMultipleTypeArgs(Generic[_T, _T2, _T3]): ... + + +class SubclassGenericMultipleTypeArgs(BaseGenericMultipleTypeArgs[_T, _T2, _T3]): ... + + +class SubclassDifferentOrderGenericMultipleTypeArgs(BaseGenericMultipleTypeArgs[_T2, _T, _T3]): ... + + +def test_extract_type_var() -> None: + assert ( + extract_type_var_from_base( + BaseGeneric[int], + index=0, + generic_bases=cast("tuple[type, ...]", (BaseGeneric,)), + ) + == int + ) + + +def test_extract_type_var_generic_subclass() -> None: + assert ( + extract_type_var_from_base( + SubclassGeneric[int], + index=0, + generic_bases=cast("tuple[type, ...]", (BaseGeneric,)), + ) + == int + ) + + +def test_extract_type_var_multiple() -> None: + typ = BaseGenericMultipleTypeArgs[int, str, None] + + generic_bases = cast("tuple[type, ...]", (BaseGenericMultipleTypeArgs,)) + assert extract_type_var_from_base(typ, index=0, generic_bases=generic_bases) == int + assert extract_type_var_from_base(typ, index=1, generic_bases=generic_bases) == str + assert extract_type_var_from_base(typ, index=2, generic_bases=generic_bases) == type(None) + + +def test_extract_type_var_generic_subclass_multiple() -> None: + typ = SubclassGenericMultipleTypeArgs[int, str, None] + + generic_bases = cast("tuple[type, ...]", (BaseGenericMultipleTypeArgs,)) + assert extract_type_var_from_base(typ, index=0, generic_bases=generic_bases) == int + assert extract_type_var_from_base(typ, index=1, generic_bases=generic_bases) == str + assert extract_type_var_from_base(typ, index=2, generic_bases=generic_bases) == type(None) + + +def test_extract_type_var_generic_subclass_different_ordering_multiple() -> None: + typ = SubclassDifferentOrderGenericMultipleTypeArgs[int, str, None] + + generic_bases = cast("tuple[type, ...]", (BaseGenericMultipleTypeArgs,)) + assert extract_type_var_from_base(typ, index=0, generic_bases=generic_bases) == int + assert extract_type_var_from_base(typ, index=1, generic_bases=generic_bases) == str + assert extract_type_var_from_base(typ, index=2, generic_bases=generic_bases) == type(None) diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..ecdcb91 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,155 @@ +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 agility._types import Omit, NoneType +from agility._utils import ( + is_dict, + is_list, + is_list_type, + is_union_type, + extract_type_arg, + is_annotated_type, +) +from agility._compat import PYDANTIC_V2, field_outer_type, get_model_fields +from agility._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 | Omit) -> Iterator[None]: + old = os.environ.copy() + + try: + for name, value in new_env.items(): + if isinstance(value, Omit): + os.environ.pop(name, None) + else: + os.environ[name] = value + + yield None + finally: + os.environ.clear() + os.environ.update(old) From 302005355b6c622f42278a813a9ae2f508ef55ce Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Sun, 13 Oct 2024 23:24:54 +0000 Subject: [PATCH 03/77] feat(api): api update --- .stats.yml | 2 +- README.md | 6 +- src/agility/_client.py | 102 +------------ tests/conftest.py | 18 +-- tests/test_client.py | 328 +++++------------------------------------ 5 files changed, 50 insertions(+), 406 deletions(-) diff --git a/.stats.yml b/.stats.yml index 51df2cf..b957b41 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 38 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-c2935bcab51a9d09489e4486ff6a9ae450dee07fbbd025136a9174febdb91ccc.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-545a995322d24ba70cd2e52703fe1e6422a02fe73623439a2815f1d55142edff.yml diff --git a/README.md b/README.md index ce39246..d74c442 100644 --- a/README.md +++ b/README.md @@ -39,10 +39,10 @@ assistant = client.assistants.create( print(assistant.id) ``` -While you can provide a `bearer_token` keyword argument, +While you can provide an `api_key` keyword argument, we recommend using [python-dotenv](https://pypi.org/project/python-dotenv/) -to add `BEARER_TOKEN="My Bearer Token"` to your `.env` file -so that your Bearer Token is not stored in source control. +to add `AUTHENTICATED_API_KEY="My API Key"` to your `.env` file +so that your API Key is not stored in source control. ## Async usage diff --git a/src/agility/_client.py b/src/agility/_client.py index 53abf1b..aa23183 100644 --- a/src/agility/_client.py +++ b/src/agility/_client.py @@ -55,16 +55,12 @@ class Agility(SyncAPIClient): with_streaming_response: AgilityWithStreamedResponse # client options - bearer_token: str api_key: str - access_key: str def __init__( self, *, - bearer_token: str | None = None, api_key: str | None = None, - access_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, @@ -86,19 +82,8 @@ def __init__( ) -> None: """Construct a new synchronous agility client instance. - This automatically infers the following arguments from their corresponding environment variables if they are not provided: - - `bearer_token` from `BEARER_TOKEN` - - `api_key` from `AUTHENTICATED_API_KEY` - - `access_key` from `PUBLIC_ACCESS_KEY` + This automatically infers the `api_key` argument from the `AUTHENTICATED_API_KEY` environment variable if it is not provided. """ - if bearer_token is None: - bearer_token = os.environ.get("BEARER_TOKEN") - if bearer_token is None: - raise AgilityError( - "The bearer_token client option must be set either by passing bearer_token to the client or by setting the BEARER_TOKEN environment variable" - ) - self.bearer_token = bearer_token - if api_key is None: api_key = os.environ.get("AUTHENTICATED_API_KEY") if api_key is None: @@ -107,18 +92,10 @@ def __init__( ) self.api_key = api_key - if access_key is None: - access_key = os.environ.get("PUBLIC_ACCESS_KEY") - if access_key is None: - raise AgilityError( - "The access_key client option must be set either by passing access_key to the client or by setting the PUBLIC_ACCESS_KEY environment variable" - ) - self.access_key = access_key - if base_url is None: base_url = os.environ.get("AGILITY_BASE_URL") if base_url is None: - base_url = f"https://localhost:8080/test-api" + base_url = f"https://localhost:8080" super().__init__( version=__version__, @@ -147,29 +124,9 @@ def qs(self) -> Querystring: @property @override def auth_headers(self) -> dict[str, str]: - if self._http_bearer: - return self._http_bearer - if self._authenticated_api_key: - return self._authenticated_api_key - if self._public_access_key: - return self._public_access_key - return {} - - @property - def _http_bearer(self) -> dict[str, str]: - bearer_token = self.bearer_token - return {"Authorization": f"Bearer {bearer_token}"} - - @property - def _authenticated_api_key(self) -> dict[str, str]: api_key = self.api_key return {"X-API-Key": api_key} - @property - def _public_access_key(self) -> dict[str, str]: - access_key = self.access_key - return {"X-Access-Key": access_key} - @property @override def default_headers(self) -> dict[str, str | Omit]: @@ -182,9 +139,7 @@ def default_headers(self) -> dict[str, str | Omit]: def copy( self, *, - bearer_token: str | None = None, api_key: str | None = None, - access_key: str | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = NOT_GIVEN, http_client: httpx.Client | None = None, @@ -218,9 +173,7 @@ def copy( http_client = http_client or self._client return self.__class__( - bearer_token=bearer_token or self.bearer_token, api_key=api_key or self.api_key, - access_key=access_key or self.access_key, base_url=base_url or self.base_url, timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, http_client=http_client, @@ -278,16 +231,12 @@ class AsyncAgility(AsyncAPIClient): with_streaming_response: AsyncAgilityWithStreamedResponse # client options - bearer_token: str api_key: str - access_key: str def __init__( self, *, - bearer_token: str | None = None, api_key: str | None = None, - access_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, @@ -309,19 +258,8 @@ def __init__( ) -> None: """Construct a new async agility client instance. - This automatically infers the following arguments from their corresponding environment variables if they are not provided: - - `bearer_token` from `BEARER_TOKEN` - - `api_key` from `AUTHENTICATED_API_KEY` - - `access_key` from `PUBLIC_ACCESS_KEY` + This automatically infers the `api_key` argument from the `AUTHENTICATED_API_KEY` environment variable if it is not provided. """ - if bearer_token is None: - bearer_token = os.environ.get("BEARER_TOKEN") - if bearer_token is None: - raise AgilityError( - "The bearer_token client option must be set either by passing bearer_token to the client or by setting the BEARER_TOKEN environment variable" - ) - self.bearer_token = bearer_token - if api_key is None: api_key = os.environ.get("AUTHENTICATED_API_KEY") if api_key is None: @@ -330,18 +268,10 @@ def __init__( ) self.api_key = api_key - if access_key is None: - access_key = os.environ.get("PUBLIC_ACCESS_KEY") - if access_key is None: - raise AgilityError( - "The access_key client option must be set either by passing access_key to the client or by setting the PUBLIC_ACCESS_KEY environment variable" - ) - self.access_key = access_key - if base_url is None: base_url = os.environ.get("AGILITY_BASE_URL") if base_url is None: - base_url = f"https://localhost:8080/test-api" + base_url = f"https://localhost:8080" super().__init__( version=__version__, @@ -370,29 +300,9 @@ def qs(self) -> Querystring: @property @override def auth_headers(self) -> dict[str, str]: - if self._http_bearer: - return self._http_bearer - if self._authenticated_api_key: - return self._authenticated_api_key - if self._public_access_key: - return self._public_access_key - return {} - - @property - def _http_bearer(self) -> dict[str, str]: - bearer_token = self.bearer_token - return {"Authorization": f"Bearer {bearer_token}"} - - @property - def _authenticated_api_key(self) -> dict[str, str]: api_key = self.api_key return {"X-API-Key": api_key} - @property - def _public_access_key(self) -> dict[str, str]: - access_key = self.access_key - return {"X-Access-Key": access_key} - @property @override def default_headers(self) -> dict[str, str | Omit]: @@ -405,9 +315,7 @@ def default_headers(self) -> dict[str, str | Omit]: def copy( self, *, - bearer_token: str | None = None, api_key: str | None = None, - access_key: str | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = NOT_GIVEN, http_client: httpx.AsyncClient | None = None, @@ -441,9 +349,7 @@ def copy( http_client = http_client or self._client return self.__class__( - bearer_token=bearer_token or self.bearer_token, api_key=api_key or self.api_key, - access_key=access_key or self.access_key, base_url=base_url or self.base_url, timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, http_client=http_client, diff --git a/tests/conftest.py b/tests/conftest.py index e6eb3e1..8421e24 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -26,9 +26,7 @@ def event_loop() -> Iterator[asyncio.AbstractEventLoop]: base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") -bearer_token = "My Bearer Token" api_key = "My API Key" -access_key = "My Access Key" @pytest.fixture(scope="session") @@ -37,13 +35,7 @@ def client(request: FixtureRequest) -> Iterator[Agility]: if not isinstance(strict, bool): raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") - with Agility( - base_url=base_url, - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=strict, - ) as client: + with Agility(base_url=base_url, api_key=api_key, _strict_response_validation=strict) as client: yield client @@ -53,11 +45,5 @@ async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncAgility]: if not isinstance(strict, bool): raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") - async with AsyncAgility( - base_url=base_url, - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=strict, - ) as client: + async with AsyncAgility(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 62f7e0a..c62b67c 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -31,9 +31,7 @@ from .utils import update_env base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") -bearer_token = "My Bearer Token" api_key = "My API Key" -access_key = "My Access Key" def _get_params(client: BaseClient[Any, Any]) -> dict[str, str]: @@ -55,13 +53,7 @@ def _get_open_connections(client: Agility | AsyncAgility) -> int: class TestAgility: - client = Agility( - base_url=base_url, - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=True, - ) + client = Agility(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: @@ -87,18 +79,10 @@ def test_copy(self) -> None: copied = self.client.copy() assert id(copied) != id(self.client) - copied = self.client.copy(bearer_token="another My Bearer Token") - assert copied.bearer_token == "another My Bearer Token" - assert self.client.bearer_token == "My Bearer Token" - 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" - copied = self.client.copy(access_key="another My Access Key") - assert copied.access_key == "another My Access Key" - assert self.client.access_key == "My Access Key" - def test_copy_default_options(self) -> None: # options that have a default are overridden correctly copied = self.client.copy(max_retries=7) @@ -117,12 +101,7 @@ def test_copy_default_options(self) -> None: def test_copy_default_headers(self) -> None: client = Agility( - base_url=base_url, - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=True, - default_headers={"X-Foo": "bar"}, + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) assert client.default_headers["X-Foo"] == "bar" @@ -156,12 +135,7 @@ def test_copy_default_headers(self) -> None: def test_copy_default_query(self) -> None: client = Agility( - base_url=base_url, - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=True, - default_query={"foo": "bar"}, + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"} ) assert _get_params(client)["foo"] == "bar" @@ -285,14 +259,7 @@ def test_request_timeout(self) -> None: assert timeout == httpx.Timeout(100.0) def test_client_timeout_option(self) -> None: - client = Agility( - base_url=base_url, - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=True, - timeout=httpx.Timeout(0), - ) + client = Agility(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 @@ -302,12 +269,7 @@ 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 = Agility( - base_url=base_url, - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=True, - http_client=http_client, + 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")) @@ -317,12 +279,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 = Agility( - base_url=base_url, - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=True, - http_client=http_client, + 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")) @@ -332,12 +289,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 = Agility( - base_url=base_url, - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=True, - http_client=http_client, + 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")) @@ -349,21 +301,14 @@ async def test_invalid_http_client(self) -> None: async with httpx.AsyncClient() as http_client: Agility( base_url=base_url, - bearer_token=bearer_token, api_key=api_key, - access_key=access_key, _strict_response_validation=True, http_client=cast(Any, http_client), ) def test_default_headers_option(self) -> None: client = Agility( - base_url=base_url, - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=True, - default_headers={"X-Foo": "bar"}, + 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" @@ -371,9 +316,7 @@ def test_default_headers_option(self) -> None: client2 = Agility( base_url=base_url, - bearer_token=bearer_token, api_key=api_key, - access_key=access_key, _strict_response_validation=True, default_headers={ "X-Foo": "stainless", @@ -386,12 +329,7 @@ def test_default_headers_option(self) -> None: def test_default_query_option(self) -> None: client = Agility( - base_url=base_url, - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=True, - default_query={"query_param": "bar"}, + 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) @@ -591,13 +529,7 @@ class Model(BaseModel): assert response.foo == 2 def test_base_url_setter(self) -> None: - client = Agility( - base_url="https://example.com/from_init", - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=True, - ) + client = Agility(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] @@ -606,26 +538,16 @@ def test_base_url_setter(self) -> None: def test_base_url_env(self) -> None: with update_env(AGILITY_BASE_URL="http://localhost:5000/from/env"): - client = Agility( - bearer_token=bearer_token, api_key=api_key, access_key=access_key, _strict_response_validation=True - ) + client = Agility(api_key=api_key, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" @pytest.mark.parametrize( "client", [ + Agility(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), Agility( base_url="http://localhost:5000/custom/path/", - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=True, - ), - Agility( - base_url="http://localhost:5000/custom/path/", - bearer_token=bearer_token, api_key=api_key, - access_key=access_key, _strict_response_validation=True, http_client=httpx.Client(), ), @@ -645,18 +567,10 @@ def test_base_url_trailing_slash(self, client: Agility) -> None: @pytest.mark.parametrize( "client", [ + Agility(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), Agility( base_url="http://localhost:5000/custom/path/", - bearer_token=bearer_token, api_key=api_key, - access_key=access_key, - _strict_response_validation=True, - ), - Agility( - base_url="http://localhost:5000/custom/path/", - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, _strict_response_validation=True, http_client=httpx.Client(), ), @@ -676,18 +590,10 @@ def test_base_url_no_trailing_slash(self, client: Agility) -> None: @pytest.mark.parametrize( "client", [ + Agility(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), Agility( base_url="http://localhost:5000/custom/path/", - bearer_token=bearer_token, api_key=api_key, - access_key=access_key, - _strict_response_validation=True, - ), - Agility( - base_url="http://localhost:5000/custom/path/", - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, _strict_response_validation=True, http_client=httpx.Client(), ), @@ -705,13 +611,7 @@ def test_absolute_request_url(self, client: Agility) -> None: assert request.url == "https://myapi.com/foo" def test_copied_client_does_not_close_http(self) -> None: - client = Agility( - base_url=base_url, - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=True, - ) + client = Agility(base_url=base_url, api_key=api_key, _strict_response_validation=True) assert not client.is_closed() copied = client.copy() @@ -722,13 +622,7 @@ def test_copied_client_does_not_close_http(self) -> None: assert not client.is_closed() def test_client_context_manager(self) -> None: - client = Agility( - base_url=base_url, - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=True, - ) + client = Agility(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() @@ -749,14 +643,7 @@ class Model(BaseModel): def test_client_max_retries_validation(self) -> None: with pytest.raises(TypeError, match=r"max_retries cannot be None"): - Agility( - base_url=base_url, - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=True, - max_retries=cast(Any, None), - ) + Agility(base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None)) @pytest.mark.respx(base_url=base_url) def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None: @@ -765,24 +652,12 @@ class Model(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) - strict_client = Agility( - base_url=base_url, - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=True, - ) + strict_client = Agility(base_url=base_url, api_key=api_key, _strict_response_validation=True) with pytest.raises(APIResponseValidationError): strict_client.get("/foo", cast_to=Model) - client = Agility( - base_url=base_url, - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=False, - ) + client = Agility(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] @@ -810,13 +685,7 @@ 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 = Agility( - base_url=base_url, - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=True, - ) + client = Agility(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) @@ -945,13 +814,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: class TestAsyncAgility: - client = AsyncAgility( - base_url=base_url, - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=True, - ) + client = AsyncAgility(base_url=base_url, api_key=api_key, _strict_response_validation=True) @pytest.mark.respx(base_url=base_url) @pytest.mark.asyncio @@ -979,18 +842,10 @@ def test_copy(self) -> None: copied = self.client.copy() assert id(copied) != id(self.client) - copied = self.client.copy(bearer_token="another My Bearer Token") - assert copied.bearer_token == "another My Bearer Token" - assert self.client.bearer_token == "My Bearer Token" - 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" - copied = self.client.copy(access_key="another My Access Key") - assert copied.access_key == "another My Access Key" - assert self.client.access_key == "My Access Key" - def test_copy_default_options(self) -> None: # options that have a default are overridden correctly copied = self.client.copy(max_retries=7) @@ -1009,12 +864,7 @@ def test_copy_default_options(self) -> None: def test_copy_default_headers(self) -> None: client = AsyncAgility( - base_url=base_url, - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=True, - default_headers={"X-Foo": "bar"}, + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) assert client.default_headers["X-Foo"] == "bar" @@ -1048,12 +898,7 @@ def test_copy_default_headers(self) -> None: def test_copy_default_query(self) -> None: client = AsyncAgility( - base_url=base_url, - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=True, - default_query={"foo": "bar"}, + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"} ) assert _get_params(client)["foo"] == "bar" @@ -1178,12 +1023,7 @@ async def test_request_timeout(self) -> None: async def test_client_timeout_option(self) -> None: client = AsyncAgility( - base_url=base_url, - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=True, - timeout=httpx.Timeout(0), + 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")) @@ -1194,12 +1034,7 @@ 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 = AsyncAgility( - base_url=base_url, - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=True, - http_client=http_client, + 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")) @@ -1209,12 +1044,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 = AsyncAgility( - base_url=base_url, - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=True, - http_client=http_client, + 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")) @@ -1224,12 +1054,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 = AsyncAgility( - base_url=base_url, - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=True, - http_client=http_client, + 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")) @@ -1241,21 +1066,14 @@ def test_invalid_http_client(self) -> None: with httpx.Client() as http_client: AsyncAgility( base_url=base_url, - bearer_token=bearer_token, api_key=api_key, - access_key=access_key, _strict_response_validation=True, http_client=cast(Any, http_client), ) def test_default_headers_option(self) -> None: client = AsyncAgility( - base_url=base_url, - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=True, - default_headers={"X-Foo": "bar"}, + 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" @@ -1263,9 +1081,7 @@ def test_default_headers_option(self) -> None: client2 = AsyncAgility( base_url=base_url, - bearer_token=bearer_token, api_key=api_key, - access_key=access_key, _strict_response_validation=True, default_headers={ "X-Foo": "stainless", @@ -1278,12 +1094,7 @@ def test_default_headers_option(self) -> None: def test_default_query_option(self) -> None: client = AsyncAgility( - base_url=base_url, - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=True, - default_query={"query_param": "bar"}, + 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) @@ -1484,11 +1295,7 @@ class Model(BaseModel): def test_base_url_setter(self) -> None: client = AsyncAgility( - base_url="https://example.com/from_init", - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=True, + base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True ) assert client.base_url == "https://example.com/from_init/" @@ -1498,26 +1305,18 @@ def test_base_url_setter(self) -> None: def test_base_url_env(self) -> None: with update_env(AGILITY_BASE_URL="http://localhost:5000/from/env"): - client = AsyncAgility( - bearer_token=bearer_token, api_key=api_key, access_key=access_key, _strict_response_validation=True - ) + client = AsyncAgility(api_key=api_key, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" @pytest.mark.parametrize( "client", [ AsyncAgility( - base_url="http://localhost:5000/custom/path/", - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=True, + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True ), AsyncAgility( base_url="http://localhost:5000/custom/path/", - bearer_token=bearer_token, api_key=api_key, - access_key=access_key, _strict_response_validation=True, http_client=httpx.AsyncClient(), ), @@ -1538,17 +1337,11 @@ def test_base_url_trailing_slash(self, client: AsyncAgility) -> None: "client", [ AsyncAgility( - base_url="http://localhost:5000/custom/path/", - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=True, + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True ), AsyncAgility( base_url="http://localhost:5000/custom/path/", - bearer_token=bearer_token, api_key=api_key, - access_key=access_key, _strict_response_validation=True, http_client=httpx.AsyncClient(), ), @@ -1569,17 +1362,11 @@ def test_base_url_no_trailing_slash(self, client: AsyncAgility) -> None: "client", [ AsyncAgility( - base_url="http://localhost:5000/custom/path/", - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=True, + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True ), AsyncAgility( base_url="http://localhost:5000/custom/path/", - bearer_token=bearer_token, api_key=api_key, - access_key=access_key, _strict_response_validation=True, http_client=httpx.AsyncClient(), ), @@ -1597,13 +1384,7 @@ def test_absolute_request_url(self, client: AsyncAgility) -> None: assert request.url == "https://myapi.com/foo" async def test_copied_client_does_not_close_http(self) -> None: - client = AsyncAgility( - base_url=base_url, - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=True, - ) + client = AsyncAgility(base_url=base_url, api_key=api_key, _strict_response_validation=True) assert not client.is_closed() copied = client.copy() @@ -1615,13 +1396,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 = AsyncAgility( - base_url=base_url, - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=True, - ) + client = AsyncAgility(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() @@ -1644,12 +1419,7 @@ class Model(BaseModel): async def test_client_max_retries_validation(self) -> None: with pytest.raises(TypeError, match=r"max_retries cannot be None"): AsyncAgility( - base_url=base_url, - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=True, - max_retries=cast(Any, None), + base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None) ) @pytest.mark.respx(base_url=base_url) @@ -1660,24 +1430,12 @@ class Model(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) - strict_client = AsyncAgility( - base_url=base_url, - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=True, - ) + strict_client = AsyncAgility(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 = AsyncAgility( - base_url=base_url, - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=False, - ) + client = AsyncAgility(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] @@ -1706,13 +1464,7 @@ 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 = AsyncAgility( - base_url=base_url, - bearer_token=bearer_token, - api_key=api_key, - access_key=access_key, - _strict_response_validation=True, - ) + client = AsyncAgility(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) From c79e481f0730e02ff441e95f9f5097712060877d Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Sun, 13 Oct 2024 23:27:06 +0000 Subject: [PATCH 04/77] feat(api): api update --- src/agility/_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/agility/_client.py b/src/agility/_client.py index aa23183..2119303 100644 --- a/src/agility/_client.py +++ b/src/agility/_client.py @@ -95,7 +95,7 @@ def __init__( if base_url is None: base_url = os.environ.get("AGILITY_BASE_URL") if base_url is None: - base_url = f"https://localhost:8080" + base_url = f"http://localhost:8080" super().__init__( version=__version__, @@ -271,7 +271,7 @@ def __init__( if base_url is None: base_url = os.environ.get("AGILITY_BASE_URL") if base_url is None: - base_url = f"https://localhost:8080" + base_url = f"http://localhost:8080" super().__init__( version=__version__, From 33b60fb9411e8b2a57c6de873fbf6000fd558b50 Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Wed, 16 Oct 2024 23:53:53 +0000 Subject: [PATCH 05/77] feat(api): api update --- .stats.yml | 2 +- README.md | 8 +- api.md | 4 +- .../resources/assistants/assistants.py | 19 +- src/agility/resources/threads/runs.py | 8 + src/agility/types/__init__.py | 1 - src/agility/types/assistant_create_params.py | 35 +++- src/agility/types/assistant_update_params.py | 35 +++- src/agility/types/assistant_with_config.py | 35 +++- .../types/knowledge_base_create_params.py | 4 + .../types/knowledge_base_update_params.py | 4 + .../types/knowledge_base_with_config.py | 4 + src/agility/types/knowledge_bases/source.py | 22 ++- .../knowledge_bases/source_create_params.py | 13 +- .../knowledge_bases/source_update_params.py | 13 +- .../types/knowledge_bases/sources/document.py | 6 +- src/agility/types/threads/run.py | 37 +++- .../types/threads/run_create_params.py | 42 +++- .../types/threads/run_stream_params.py | 42 +++- tests/api_resources/test_assistants.py | 181 +++++++++++++++++- tests/api_resources/threads/test_runs.py | 164 ++++++++++++++++ 21 files changed, 635 insertions(+), 44 deletions(-) diff --git a/.stats.yml b/.stats.yml index b957b41..aca6e88 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 38 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-545a995322d24ba70cd2e52703fe1e6422a02fe73623439a2815f1d55142edff.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-ef07edf2d4acad0ca9e33a8068f3d32de9edd59e16e573304cf8989a165c77ab.yml diff --git a/README.md b/README.md index d74c442..beff70f 100644 --- a/README.md +++ b/README.md @@ -31,12 +31,12 @@ from agility import Agility client = Agility() -assistant = client.assistants.create( +assistant_with_config = client.assistants.create( description="description", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", name="name", ) -print(assistant.id) +print(assistant_with_config.id) ``` While you can provide an `api_key` keyword argument, @@ -56,12 +56,12 @@ client = AsyncAgility() async def main() -> None: - assistant = await client.assistants.create( + assistant_with_config = await client.assistants.create( description="description", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", name="name", ) - print(assistant.id) + print(assistant_with_config.id) asyncio.run(main()) diff --git a/api.md b/api.md index e2533b5..06687b2 100644 --- a/api.md +++ b/api.md @@ -3,12 +3,12 @@ Types: ```python -from agility.types import Assistant, AssistantWithConfig, AssistantListResponse +from agility.types import AssistantWithConfig, AssistantListResponse ``` Methods: -- client.assistants.create(\*\*params) -> Assistant +- client.assistants.create(\*\*params) -> AssistantWithConfig - client.assistants.retrieve(assistant_id) -> AssistantWithConfig - client.assistants.update(assistant_id, \*\*params) -> AssistantWithConfig - client.assistants.list(\*\*params) -> AssistantListResponse diff --git a/src/agility/resources/assistants/assistants.py b/src/agility/resources/assistants/assistants.py index 111bf93..6fdba2e 100644 --- a/src/agility/resources/assistants/assistants.py +++ b/src/agility/resources/assistants/assistants.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Optional +from typing import Iterable, Optional from typing_extensions import Literal import httpx @@ -30,7 +30,6 @@ AsyncAccessKeysResourceWithStreamingResponse, ) from ..._base_client import make_request_options -from ...types.assistant import Assistant from ...types.assistant_with_config import AssistantWithConfig from ...types.assistant_list_response import AssistantListResponse @@ -69,13 +68,14 @@ def create( name: str, instructions: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, + tools: Iterable[assistant_create_params.Tool] | 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, - ) -> Assistant: + ) -> AssistantWithConfig: """ Create a new assistant. @@ -97,13 +97,14 @@ def create( "name": name, "instructions": instructions, "model": model, + "tools": tools, }, assistant_create_params.AssistantCreateParams, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Assistant, + cast_to=AssistantWithConfig, ) def retrieve( @@ -149,6 +150,7 @@ def update( name: str, instructions: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, + tools: Iterable[assistant_update_params.Tool] | 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, @@ -180,6 +182,7 @@ def update( "name": name, "instructions": instructions, "model": model, + "tools": tools, }, assistant_update_params.AssistantUpdateParams, ), @@ -298,13 +301,14 @@ async def create( name: str, instructions: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, + tools: Iterable[assistant_create_params.Tool] | 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, - ) -> Assistant: + ) -> AssistantWithConfig: """ Create a new assistant. @@ -326,13 +330,14 @@ async def create( "name": name, "instructions": instructions, "model": model, + "tools": tools, }, assistant_create_params.AssistantCreateParams, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Assistant, + cast_to=AssistantWithConfig, ) async def retrieve( @@ -378,6 +383,7 @@ async def update( name: str, instructions: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, + tools: Iterable[assistant_update_params.Tool] | 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, @@ -409,6 +415,7 @@ async def update( "name": name, "instructions": instructions, "model": model, + "tools": tools, }, assistant_update_params.AssistantUpdateParams, ), diff --git a/src/agility/resources/threads/runs.py b/src/agility/resources/threads/runs.py index f76a5f7..aaf4330 100644 --- a/src/agility/resources/threads/runs.py +++ b/src/agility/resources/threads/runs.py @@ -57,6 +57,7 @@ def create( instructions: Optional[str] | NotGiven = NOT_GIVEN, knowledge_base_id: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[run_create_params.Tool]] | 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, @@ -88,6 +89,7 @@ def create( "instructions": instructions, "knowledge_base_id": knowledge_base_id, "model": model, + "tools": tools, }, run_create_params.RunCreateParams, ), @@ -180,6 +182,7 @@ def stream( instructions: Optional[str] | NotGiven = NOT_GIVEN, knowledge_base_id: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[run_stream_params.Tool]] | 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, @@ -211,6 +214,7 @@ def stream( "instructions": instructions, "knowledge_base_id": knowledge_base_id, "model": model, + "tools": tools, }, run_stream_params.RunStreamParams, ), @@ -251,6 +255,7 @@ async def create( instructions: Optional[str] | NotGiven = NOT_GIVEN, knowledge_base_id: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[run_create_params.Tool]] | 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, @@ -282,6 +287,7 @@ async def create( "instructions": instructions, "knowledge_base_id": knowledge_base_id, "model": model, + "tools": tools, }, run_create_params.RunCreateParams, ), @@ -374,6 +380,7 @@ async def stream( instructions: Optional[str] | NotGiven = NOT_GIVEN, knowledge_base_id: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[run_stream_params.Tool]] | 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, @@ -405,6 +412,7 @@ async def stream( "instructions": instructions, "knowledge_base_id": knowledge_base_id, "model": model, + "tools": tools, }, run_stream_params.RunStreamParams, ), diff --git a/src/agility/types/__init__.py b/src/agility/types/__init__.py index 791d45a..2124b90 100644 --- a/src/agility/types/__init__.py +++ b/src/agility/types/__init__.py @@ -4,7 +4,6 @@ from .user import User as User from .thread import Thread as Thread -from .assistant import Assistant as Assistant from .thread_list_params import ThreadListParams as ThreadListParams from .thread_list_response import ThreadListResponse as ThreadListResponse from .assistant_list_params import AssistantListParams as AssistantListParams diff --git a/src/agility/types/assistant_create_params.py b/src/agility/types/assistant_create_params.py index e105b20..f70d65c 100644 --- a/src/agility/types/assistant_create_params.py +++ b/src/agility/types/assistant_create_params.py @@ -2,10 +2,10 @@ from __future__ import annotations -from typing import Optional +from typing import Dict, List, Iterable, Optional from typing_extensions import Literal, Required, TypedDict -__all__ = ["AssistantCreateParams"] +__all__ = ["AssistantCreateParams", "Tool", "ToolFunction", "ToolFunctionParameters"] class AssistantCreateParams(TypedDict, total=False): @@ -18,3 +18,34 @@ class AssistantCreateParams(TypedDict, total=False): instructions: Optional[str] model: Optional[Literal["gpt-4o"]] + + tools: Iterable[Tool] + + +class ToolFunctionParameters(TypedDict, total=False): + type: Required[str] + + properties: Dict[str, object] + + required: Optional[List[str]] + + +class ToolFunction(TypedDict, total=False): + description: Required[str] + """ + A description of what the function does, used by the model to choose when and + how to call the function. + """ + + name: Required[str] + """The name of the function to be called.""" + + parameters: Optional[ToolFunctionParameters] + + strict: bool + + +class Tool(TypedDict, total=False): + function: Required[ToolFunction] + + type: Required[Literal["function"]] diff --git a/src/agility/types/assistant_update_params.py b/src/agility/types/assistant_update_params.py index 33fe30b..71cb1e2 100644 --- a/src/agility/types/assistant_update_params.py +++ b/src/agility/types/assistant_update_params.py @@ -2,10 +2,10 @@ from __future__ import annotations -from typing import Optional +from typing import Dict, List, Iterable, Optional from typing_extensions import Literal, Required, TypedDict -__all__ = ["AssistantUpdateParams"] +__all__ = ["AssistantUpdateParams", "Tool", "ToolFunction", "ToolFunctionParameters"] class AssistantUpdateParams(TypedDict, total=False): @@ -20,3 +20,34 @@ class AssistantUpdateParams(TypedDict, total=False): instructions: Optional[str] model: Optional[Literal["gpt-4o"]] + + tools: Iterable[Tool] + + +class ToolFunctionParameters(TypedDict, total=False): + type: Required[str] + + properties: Dict[str, object] + + required: Optional[List[str]] + + +class ToolFunction(TypedDict, total=False): + description: Required[str] + """ + A description of what the function does, used by the model to choose when and + how to call the function. + """ + + name: Required[str] + """The name of the function to be called.""" + + parameters: Optional[ToolFunctionParameters] + + strict: bool + + +class Tool(TypedDict, total=False): + function: Required[ToolFunction] + + type: Required[Literal["function"]] diff --git a/src/agility/types/assistant_with_config.py b/src/agility/types/assistant_with_config.py index 4fe0d2f..3a72b7c 100644 --- a/src/agility/types/assistant_with_config.py +++ b/src/agility/types/assistant_with_config.py @@ -1,12 +1,41 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional +from typing import Dict, List, Optional from datetime import datetime from typing_extensions import Literal from .._models import BaseModel -__all__ = ["AssistantWithConfig"] +__all__ = ["AssistantWithConfig", "Tool", "ToolFunction", "ToolFunctionParameters"] + + +class ToolFunctionParameters(BaseModel): + type: str + + properties: Optional[Dict[str, object]] = None + + required: Optional[List[str]] = None + + +class ToolFunction(BaseModel): + description: str + """ + A description of what the function does, used by the model to choose when and + how to call the function. + """ + + name: str + """The name of the function to be called.""" + + parameters: Optional[ToolFunctionParameters] = None + + strict: Optional[bool] = None + + +class Tool(BaseModel): + function: ToolFunction + + type: Literal["function"] class AssistantWithConfig(BaseModel): @@ -27,3 +56,5 @@ class AssistantWithConfig(BaseModel): instructions: Optional[str] = None model: Optional[Literal["gpt-4o"]] = None + + tools: Optional[List[Tool]] = None diff --git a/src/agility/types/knowledge_base_create_params.py b/src/agility/types/knowledge_base_create_params.py index 5f823bf..3ec2349 100644 --- a/src/agility/types/knowledge_base_create_params.py +++ b/src/agility/types/knowledge_base_create_params.py @@ -88,6 +88,10 @@ class IngestionPipelineParamsTransform(TypedDict, total=False): class IngestionPipelineParamsVectorStore(TypedDict, total=False): weaviate_collection_name: Required[str] + """The name of the Weaviate collection to use for storing documents. + + Must start with AgilityKB and be valid. + """ node_tags: Dict[str, str] diff --git a/src/agility/types/knowledge_base_update_params.py b/src/agility/types/knowledge_base_update_params.py index 68c055b..6c2339e 100644 --- a/src/agility/types/knowledge_base_update_params.py +++ b/src/agility/types/knowledge_base_update_params.py @@ -88,6 +88,10 @@ class IngestionPipelineParamsTransform(TypedDict, total=False): class IngestionPipelineParamsVectorStore(TypedDict, total=False): weaviate_collection_name: Required[str] + """The name of the Weaviate collection to use for storing documents. + + Must start with AgilityKB and be valid. + """ node_tags: Dict[str, str] diff --git a/src/agility/types/knowledge_base_with_config.py b/src/agility/types/knowledge_base_with_config.py index 9fe414c..6aa07b6 100644 --- a/src/agility/types/knowledge_base_with_config.py +++ b/src/agility/types/knowledge_base_with_config.py @@ -81,6 +81,10 @@ class IngestionPipelineParamsTransform(BaseModel): class IngestionPipelineParamsVectorStore(BaseModel): weaviate_collection_name: str + """The name of the Weaviate collection to use for storing documents. + + Must start with AgilityKB and be valid. + """ node_tags: Optional[Dict[str, str]] = None diff --git a/src/agility/types/knowledge_bases/source.py b/src/agility/types/knowledge_bases/source.py index 620955b..83497a9 100644 --- a/src/agility/types/knowledge_bases/source.py +++ b/src/agility/types/knowledge_bases/source.py @@ -7,7 +7,14 @@ from ..._utils import PropertyInfo from ..._models import BaseModel -__all__ = ["Source", "SourceParams", "SourceParamsWebV0Params", "SourceParamsNotionParams", "SourceSchedule"] +__all__ = [ + "Source", + "SourceParams", + "SourceParamsWebV0Params", + "SourceParamsNotionParams", + "SourceParamsS3PublicV0Params", + "SourceSchedule", +] class SourceParamsWebV0Params(BaseModel): @@ -32,8 +39,19 @@ class SourceParamsNotionParams(BaseModel): name: Optional[Literal["notion"]] = None +class SourceParamsS3PublicV0Params(BaseModel): + bucket_name: str + + limit: int + + prefix: str + + name: Optional[Literal["s3_public_v0"]] = None + + SourceParams: TypeAlias = Annotated[ - Union[SourceParamsWebV0Params, SourceParamsNotionParams], PropertyInfo(discriminator="name") + Union[SourceParamsWebV0Params, SourceParamsNotionParams, SourceParamsS3PublicV0Params], + PropertyInfo(discriminator="name"), ] diff --git a/src/agility/types/knowledge_bases/source_create_params.py b/src/agility/types/knowledge_bases/source_create_params.py index ba1daf0..ccdf6b5 100644 --- a/src/agility/types/knowledge_bases/source_create_params.py +++ b/src/agility/types/knowledge_bases/source_create_params.py @@ -10,6 +10,7 @@ "SourceParams", "SourceParamsWebV0Params", "SourceParamsNotionParams", + "SourceParamsS3PublicV0Params", "SourceSchedule", ] @@ -48,7 +49,17 @@ class SourceParamsNotionParams(TypedDict, total=False): name: Literal["notion"] -SourceParams: TypeAlias = Union[SourceParamsWebV0Params, SourceParamsNotionParams] +class SourceParamsS3PublicV0Params(TypedDict, total=False): + bucket_name: Required[str] + + limit: Required[int] + + prefix: Required[str] + + name: Literal["s3_public_v0"] + + +SourceParams: TypeAlias = Union[SourceParamsWebV0Params, SourceParamsNotionParams, SourceParamsS3PublicV0Params] class SourceSchedule(TypedDict, total=False): diff --git a/src/agility/types/knowledge_bases/source_update_params.py b/src/agility/types/knowledge_bases/source_update_params.py index dd26d39..eb5034b 100644 --- a/src/agility/types/knowledge_bases/source_update_params.py +++ b/src/agility/types/knowledge_bases/source_update_params.py @@ -10,6 +10,7 @@ "SourceParams", "SourceParamsWebV0Params", "SourceParamsNotionParams", + "SourceParamsS3PublicV0Params", "SourceSchedule", ] @@ -50,7 +51,17 @@ class SourceParamsNotionParams(TypedDict, total=False): name: Literal["notion"] -SourceParams: TypeAlias = Union[SourceParamsWebV0Params, SourceParamsNotionParams] +class SourceParamsS3PublicV0Params(TypedDict, total=False): + bucket_name: Required[str] + + limit: Required[int] + + prefix: Required[str] + + name: Literal["s3_public_v0"] + + +SourceParams: TypeAlias = Union[SourceParamsWebV0Params, SourceParamsNotionParams, SourceParamsS3PublicV0Params] class SourceSchedule(TypedDict, total=False): diff --git a/src/agility/types/knowledge_bases/sources/document.py b/src/agility/types/knowledge_bases/sources/document.py index 93eb686..a2b98de 100644 --- a/src/agility/types/knowledge_bases/sources/document.py +++ b/src/agility/types/knowledge_bases/sources/document.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List, Optional +from typing import List, Union, Optional from datetime import datetime from ...._models import BaseModel @@ -11,7 +11,7 @@ class Metadata(BaseModel): key: str - value: str + value: Union[str, float, bool, None] = None class Document(BaseModel): @@ -29,6 +29,4 @@ class Document(BaseModel): source_id: str - title: str - updated_at: datetime diff --git a/src/agility/types/threads/run.py b/src/agility/types/threads/run.py index 192adc5..b4e380a 100644 --- a/src/agility/types/threads/run.py +++ b/src/agility/types/threads/run.py @@ -1,12 +1,41 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional +from typing import Dict, List, Optional from datetime import datetime from typing_extensions import Literal from ..._models import BaseModel -__all__ = ["Run", "Usage"] +__all__ = ["Run", "Tool", "ToolFunction", "ToolFunctionParameters", "Usage"] + + +class ToolFunctionParameters(BaseModel): + type: str + + properties: Optional[Dict[str, object]] = None + + required: Optional[List[str]] = None + + +class ToolFunction(BaseModel): + description: str + """ + A description of what the function does, used by the model to choose when and + how to call the function. + """ + + name: str + """The name of the function to be called.""" + + parameters: Optional[ToolFunctionParameters] = None + + strict: Optional[bool] = None + + +class Tool(BaseModel): + function: ToolFunction + + type: Literal["function"] class Usage(BaseModel): @@ -24,7 +53,7 @@ class Run(BaseModel): created_at: datetime - status: Literal["pending", "in_progress", "completed", "failed", "canceled", "expired"] + status: Literal["pending", "in_progress", "completed", "failed", "canceled", "expired", "requires_action"] thread_id: str @@ -40,4 +69,6 @@ class Run(BaseModel): model: Optional[Literal["gpt-4o"]] = None + tools: Optional[List[Tool]] = None + usage: Optional[Usage] = None diff --git a/src/agility/types/threads/run_create_params.py b/src/agility/types/threads/run_create_params.py index b42f8a7..bb06dcb 100644 --- a/src/agility/types/threads/run_create_params.py +++ b/src/agility/types/threads/run_create_params.py @@ -2,10 +2,17 @@ from __future__ import annotations -from typing import Iterable, Optional +from typing import Dict, List, Iterable, Optional from typing_extensions import Literal, Required, TypedDict -__all__ = ["RunCreateParams", "AdditionalMessage", "AdditionalMessageMetadata"] +__all__ = [ + "RunCreateParams", + "AdditionalMessage", + "AdditionalMessageMetadata", + "Tool", + "ToolFunction", + "ToolFunctionParameters", +] class RunCreateParams(TypedDict, total=False): @@ -21,6 +28,8 @@ class RunCreateParams(TypedDict, total=False): model: Optional[Literal["gpt-4o"]] + tools: Optional[Iterable[Tool]] + class AdditionalMessageMetadata(TypedDict, total=False): trustworthiness_score: Optional[float] @@ -34,3 +43,32 @@ class AdditionalMessage(TypedDict, total=False): role: Required[Literal["user", "assistant"]] thread_id: Required[str] + + +class ToolFunctionParameters(TypedDict, total=False): + type: Required[str] + + properties: Dict[str, object] + + required: Optional[List[str]] + + +class ToolFunction(TypedDict, total=False): + description: Required[str] + """ + A description of what the function does, used by the model to choose when and + how to call the function. + """ + + name: Required[str] + """The name of the function to be called.""" + + parameters: Optional[ToolFunctionParameters] + + strict: bool + + +class Tool(TypedDict, total=False): + function: Required[ToolFunction] + + type: Required[Literal["function"]] diff --git a/src/agility/types/threads/run_stream_params.py b/src/agility/types/threads/run_stream_params.py index 804d30f..1cc770d 100644 --- a/src/agility/types/threads/run_stream_params.py +++ b/src/agility/types/threads/run_stream_params.py @@ -2,10 +2,17 @@ from __future__ import annotations -from typing import Iterable, Optional +from typing import Dict, List, Iterable, Optional from typing_extensions import Literal, Required, TypedDict -__all__ = ["RunStreamParams", "AdditionalMessage", "AdditionalMessageMetadata"] +__all__ = [ + "RunStreamParams", + "AdditionalMessage", + "AdditionalMessageMetadata", + "Tool", + "ToolFunction", + "ToolFunctionParameters", +] class RunStreamParams(TypedDict, total=False): @@ -21,6 +28,8 @@ class RunStreamParams(TypedDict, total=False): model: Optional[Literal["gpt-4o"]] + tools: Optional[Iterable[Tool]] + class AdditionalMessageMetadata(TypedDict, total=False): trustworthiness_score: Optional[float] @@ -34,3 +43,32 @@ class AdditionalMessage(TypedDict, total=False): role: Required[Literal["user", "assistant"]] thread_id: Required[str] + + +class ToolFunctionParameters(TypedDict, total=False): + type: Required[str] + + properties: Dict[str, object] + + required: Optional[List[str]] + + +class ToolFunction(TypedDict, total=False): + description: Required[str] + """ + A description of what the function does, used by the model to choose when and + how to call the function. + """ + + name: Required[str] + """The name of the function to be called.""" + + parameters: Optional[ToolFunctionParameters] + + strict: bool + + +class Tool(TypedDict, total=False): + function: Required[ToolFunction] + + type: Required[Literal["function"]] diff --git a/tests/api_resources/test_assistants.py b/tests/api_resources/test_assistants.py index 5258741..6316714 100644 --- a/tests/api_resources/test_assistants.py +++ b/tests/api_resources/test_assistants.py @@ -10,7 +10,6 @@ from agility import Agility, AsyncAgility from tests.utils import assert_matches_type from agility.types import ( - Assistant, AssistantWithConfig, AssistantListResponse, ) @@ -28,7 +27,7 @@ def test_method_create(self, client: Agility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", name="name", ) - assert_matches_type(Assistant, assistant, path=["response"]) + assert_matches_type(AssistantWithConfig, assistant, path=["response"]) @parametrize def test_method_create_with_all_params(self, client: Agility) -> None: @@ -38,8 +37,49 @@ def test_method_create_with_all_params(self, client: Agility) -> None: name="name", instructions="instructions", model="gpt-4o", + tools=[ + { + "function": { + "description": "description", + "name": "name", + "parameters": { + "type": "type", + "properties": {"foo": "bar"}, + "required": ["string", "string", "string"], + }, + "strict": True, + }, + "type": "function", + }, + { + "function": { + "description": "description", + "name": "name", + "parameters": { + "type": "type", + "properties": {"foo": "bar"}, + "required": ["string", "string", "string"], + }, + "strict": True, + }, + "type": "function", + }, + { + "function": { + "description": "description", + "name": "name", + "parameters": { + "type": "type", + "properties": {"foo": "bar"}, + "required": ["string", "string", "string"], + }, + "strict": True, + }, + "type": "function", + }, + ], ) - assert_matches_type(Assistant, assistant, path=["response"]) + assert_matches_type(AssistantWithConfig, assistant, path=["response"]) @parametrize def test_raw_response_create(self, client: Agility) -> None: @@ -52,7 +92,7 @@ def test_raw_response_create(self, client: Agility) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" assistant = response.parse() - assert_matches_type(Assistant, assistant, path=["response"]) + assert_matches_type(AssistantWithConfig, assistant, path=["response"]) @parametrize def test_streaming_response_create(self, client: Agility) -> None: @@ -65,7 +105,7 @@ def test_streaming_response_create(self, client: Agility) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" assistant = response.parse() - assert_matches_type(Assistant, assistant, path=["response"]) + assert_matches_type(AssistantWithConfig, assistant, path=["response"]) assert cast(Any, response.is_closed) is True @@ -128,6 +168,47 @@ def test_method_update_with_all_params(self, client: Agility) -> None: name="name", instructions="instructions", model="gpt-4o", + tools=[ + { + "function": { + "description": "description", + "name": "name", + "parameters": { + "type": "type", + "properties": {"foo": "bar"}, + "required": ["string", "string", "string"], + }, + "strict": True, + }, + "type": "function", + }, + { + "function": { + "description": "description", + "name": "name", + "parameters": { + "type": "type", + "properties": {"foo": "bar"}, + "required": ["string", "string", "string"], + }, + "strict": True, + }, + "type": "function", + }, + { + "function": { + "description": "description", + "name": "name", + "parameters": { + "type": "type", + "properties": {"foo": "bar"}, + "required": ["string", "string", "string"], + }, + "strict": True, + }, + "type": "function", + }, + ], ) assert_matches_type(AssistantWithConfig, assistant, path=["response"]) @@ -256,7 +337,7 @@ async def test_method_create(self, async_client: AsyncAgility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", name="name", ) - assert_matches_type(Assistant, assistant, path=["response"]) + assert_matches_type(AssistantWithConfig, assistant, path=["response"]) @parametrize async def test_method_create_with_all_params(self, async_client: AsyncAgility) -> None: @@ -266,8 +347,49 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgility) - name="name", instructions="instructions", model="gpt-4o", + tools=[ + { + "function": { + "description": "description", + "name": "name", + "parameters": { + "type": "type", + "properties": {"foo": "bar"}, + "required": ["string", "string", "string"], + }, + "strict": True, + }, + "type": "function", + }, + { + "function": { + "description": "description", + "name": "name", + "parameters": { + "type": "type", + "properties": {"foo": "bar"}, + "required": ["string", "string", "string"], + }, + "strict": True, + }, + "type": "function", + }, + { + "function": { + "description": "description", + "name": "name", + "parameters": { + "type": "type", + "properties": {"foo": "bar"}, + "required": ["string", "string", "string"], + }, + "strict": True, + }, + "type": "function", + }, + ], ) - assert_matches_type(Assistant, assistant, path=["response"]) + assert_matches_type(AssistantWithConfig, assistant, path=["response"]) @parametrize async def test_raw_response_create(self, async_client: AsyncAgility) -> None: @@ -280,7 +402,7 @@ async def test_raw_response_create(self, async_client: AsyncAgility) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" assistant = await response.parse() - assert_matches_type(Assistant, assistant, path=["response"]) + assert_matches_type(AssistantWithConfig, assistant, path=["response"]) @parametrize async def test_streaming_response_create(self, async_client: AsyncAgility) -> None: @@ -293,7 +415,7 @@ async def test_streaming_response_create(self, async_client: AsyncAgility) -> No assert response.http_request.headers.get("X-Stainless-Lang") == "python" assistant = await response.parse() - assert_matches_type(Assistant, assistant, path=["response"]) + assert_matches_type(AssistantWithConfig, assistant, path=["response"]) assert cast(Any, response.is_closed) is True @@ -356,6 +478,47 @@ async def test_method_update_with_all_params(self, async_client: AsyncAgility) - name="name", instructions="instructions", model="gpt-4o", + tools=[ + { + "function": { + "description": "description", + "name": "name", + "parameters": { + "type": "type", + "properties": {"foo": "bar"}, + "required": ["string", "string", "string"], + }, + "strict": True, + }, + "type": "function", + }, + { + "function": { + "description": "description", + "name": "name", + "parameters": { + "type": "type", + "properties": {"foo": "bar"}, + "required": ["string", "string", "string"], + }, + "strict": True, + }, + "type": "function", + }, + { + "function": { + "description": "description", + "name": "name", + "parameters": { + "type": "type", + "properties": {"foo": "bar"}, + "required": ["string", "string", "string"], + }, + "strict": True, + }, + "type": "function", + }, + ], ) assert_matches_type(AssistantWithConfig, assistant, path=["response"]) diff --git a/tests/api_resources/threads/test_runs.py b/tests/api_resources/threads/test_runs.py index d286533..b1536ee 100644 --- a/tests/api_resources/threads/test_runs.py +++ b/tests/api_resources/threads/test_runs.py @@ -54,6 +54,47 @@ def test_method_create_with_all_params(self, client: Agility) -> None: instructions="instructions", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", model="gpt-4o", + tools=[ + { + "function": { + "description": "description", + "name": "name", + "parameters": { + "type": "type", + "properties": {"foo": "bar"}, + "required": ["string", "string", "string"], + }, + "strict": True, + }, + "type": "function", + }, + { + "function": { + "description": "description", + "name": "name", + "parameters": { + "type": "type", + "properties": {"foo": "bar"}, + "required": ["string", "string", "string"], + }, + "strict": True, + }, + "type": "function", + }, + { + "function": { + "description": "description", + "name": "name", + "parameters": { + "type": "type", + "properties": {"foo": "bar"}, + "required": ["string", "string", "string"], + }, + "strict": True, + }, + "type": "function", + }, + ], ) assert_matches_type(Run, run, path=["response"]) @@ -224,6 +265,47 @@ def test_method_stream_with_all_params(self, client: Agility) -> None: instructions="instructions", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", model="gpt-4o", + tools=[ + { + "function": { + "description": "description", + "name": "name", + "parameters": { + "type": "type", + "properties": {"foo": "bar"}, + "required": ["string", "string", "string"], + }, + "strict": True, + }, + "type": "function", + }, + { + "function": { + "description": "description", + "name": "name", + "parameters": { + "type": "type", + "properties": {"foo": "bar"}, + "required": ["string", "string", "string"], + }, + "strict": True, + }, + "type": "function", + }, + { + "function": { + "description": "description", + "name": "name", + "parameters": { + "type": "type", + "properties": {"foo": "bar"}, + "required": ["string", "string", "string"], + }, + "strict": True, + }, + "type": "function", + }, + ], ) assert_matches_type(object, run, path=["response"]) @@ -302,6 +384,47 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgility) - instructions="instructions", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", model="gpt-4o", + tools=[ + { + "function": { + "description": "description", + "name": "name", + "parameters": { + "type": "type", + "properties": {"foo": "bar"}, + "required": ["string", "string", "string"], + }, + "strict": True, + }, + "type": "function", + }, + { + "function": { + "description": "description", + "name": "name", + "parameters": { + "type": "type", + "properties": {"foo": "bar"}, + "required": ["string", "string", "string"], + }, + "strict": True, + }, + "type": "function", + }, + { + "function": { + "description": "description", + "name": "name", + "parameters": { + "type": "type", + "properties": {"foo": "bar"}, + "required": ["string", "string", "string"], + }, + "strict": True, + }, + "type": "function", + }, + ], ) assert_matches_type(Run, run, path=["response"]) @@ -472,6 +595,47 @@ async def test_method_stream_with_all_params(self, async_client: AsyncAgility) - instructions="instructions", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", model="gpt-4o", + tools=[ + { + "function": { + "description": "description", + "name": "name", + "parameters": { + "type": "type", + "properties": {"foo": "bar"}, + "required": ["string", "string", "string"], + }, + "strict": True, + }, + "type": "function", + }, + { + "function": { + "description": "description", + "name": "name", + "parameters": { + "type": "type", + "properties": {"foo": "bar"}, + "required": ["string", "string", "string"], + }, + "strict": True, + }, + "type": "function", + }, + { + "function": { + "description": "description", + "name": "name", + "parameters": { + "type": "type", + "properties": {"foo": "bar"}, + "required": ["string", "string", "string"], + }, + "strict": True, + }, + "type": "function", + }, + ], ) assert_matches_type(object, run, path=["response"]) From eb969b4e976b0375fa55e1e195e2c8f0cea1eddc Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Thu, 17 Oct 2024 18:24:38 +0000 Subject: [PATCH 06/77] feat(api): api update --- .stats.yml | 2 +- .../resources/assistants/assistants.py | 8 ++-- src/agility/types/assistant_create_params.py | 8 +++- src/agility/types/assistant_update_params.py | 8 +++- src/agility/types/assistant_with_config.py | 4 ++ src/agility/types/threads/run.py | 46 ++++++++++++++++++- .../types/threads/run_create_params.py | 6 ++- .../types/threads/run_stream_params.py | 6 ++- tests/api_resources/test_assistants.py | 12 +++++ tests/api_resources/threads/test_runs.py | 12 +++++ tests/test_models.py | 2 +- 11 files changed, 101 insertions(+), 13 deletions(-) diff --git a/.stats.yml b/.stats.yml index aca6e88..813d1c9 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 38 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-ef07edf2d4acad0ca9e33a8068f3d32de9edd59e16e573304cf8989a165c77ab.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-855f9ee2331d7d0a691a87774cb55d84cb3099d6e116dfee6c4a7085e7719291.yml diff --git a/src/agility/resources/assistants/assistants.py b/src/agility/resources/assistants/assistants.py index 6fdba2e..14d5f6d 100644 --- a/src/agility/resources/assistants/assistants.py +++ b/src/agility/resources/assistants/assistants.py @@ -68,7 +68,7 @@ def create( name: str, instructions: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, - tools: Iterable[assistant_create_params.Tool] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[assistant_create_params.Tool]] | 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, @@ -150,7 +150,7 @@ def update( name: str, instructions: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, - tools: Iterable[assistant_update_params.Tool] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[assistant_update_params.Tool]] | 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, @@ -301,7 +301,7 @@ async def create( name: str, instructions: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, - tools: Iterable[assistant_create_params.Tool] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[assistant_create_params.Tool]] | 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, @@ -383,7 +383,7 @@ async def update( name: str, instructions: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, - tools: Iterable[assistant_update_params.Tool] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[assistant_update_params.Tool]] | 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/agility/types/assistant_create_params.py b/src/agility/types/assistant_create_params.py index f70d65c..8d31cde 100644 --- a/src/agility/types/assistant_create_params.py +++ b/src/agility/types/assistant_create_params.py @@ -3,7 +3,9 @@ from __future__ import annotations from typing import Dict, List, Iterable, Optional -from typing_extensions import Literal, Required, TypedDict +from typing_extensions import Literal, Required, Annotated, TypedDict + +from .._utils import PropertyInfo __all__ = ["AssistantCreateParams", "Tool", "ToolFunction", "ToolFunctionParameters"] @@ -19,12 +21,14 @@ class AssistantCreateParams(TypedDict, total=False): model: Optional[Literal["gpt-4o"]] - tools: Iterable[Tool] + tools: Optional[Iterable[Tool]] class ToolFunctionParameters(TypedDict, total=False): type: Required[str] + additional_properties: Annotated[bool, PropertyInfo(alias="additionalProperties")] + properties: Dict[str, object] required: Optional[List[str]] diff --git a/src/agility/types/assistant_update_params.py b/src/agility/types/assistant_update_params.py index 71cb1e2..9704a06 100644 --- a/src/agility/types/assistant_update_params.py +++ b/src/agility/types/assistant_update_params.py @@ -3,7 +3,9 @@ from __future__ import annotations from typing import Dict, List, Iterable, Optional -from typing_extensions import Literal, Required, TypedDict +from typing_extensions import Literal, Required, Annotated, TypedDict + +from .._utils import PropertyInfo __all__ = ["AssistantUpdateParams", "Tool", "ToolFunction", "ToolFunctionParameters"] @@ -21,12 +23,14 @@ class AssistantUpdateParams(TypedDict, total=False): model: Optional[Literal["gpt-4o"]] - tools: Iterable[Tool] + tools: Optional[Iterable[Tool]] class ToolFunctionParameters(TypedDict, total=False): type: Required[str] + additional_properties: Annotated[bool, PropertyInfo(alias="additionalProperties")] + properties: Dict[str, object] required: Optional[List[str]] diff --git a/src/agility/types/assistant_with_config.py b/src/agility/types/assistant_with_config.py index 3a72b7c..98f5029 100644 --- a/src/agility/types/assistant_with_config.py +++ b/src/agility/types/assistant_with_config.py @@ -4,6 +4,8 @@ from datetime import datetime from typing_extensions import Literal +from pydantic import Field as FieldInfo + from .._models import BaseModel __all__ = ["AssistantWithConfig", "Tool", "ToolFunction", "ToolFunctionParameters"] @@ -12,6 +14,8 @@ class ToolFunctionParameters(BaseModel): type: str + additional_properties: Optional[bool] = FieldInfo(alias="additionalProperties", default=None) + properties: Optional[Dict[str, object]] = None required: Optional[List[str]] = None diff --git a/src/agility/types/threads/run.py b/src/agility/types/threads/run.py index b4e380a..c483ff3 100644 --- a/src/agility/types/threads/run.py +++ b/src/agility/types/threads/run.py @@ -4,14 +4,56 @@ from datetime import datetime from typing_extensions import Literal +from pydantic import Field as FieldInfo + from ..._models import BaseModel -__all__ = ["Run", "Tool", "ToolFunction", "ToolFunctionParameters", "Usage"] +__all__ = [ + "Run", + "RequiredAction", + "RequiredActionSubmitToolOutputs", + "RequiredActionSubmitToolOutputsToolCall", + "RequiredActionSubmitToolOutputsToolCallFunction", + "Tool", + "ToolFunction", + "ToolFunctionParameters", + "Usage", +] + + +class RequiredActionSubmitToolOutputsToolCallFunction(BaseModel): + arguments: str + + name: str + + output: Optional[str] = None + + +class RequiredActionSubmitToolOutputsToolCall(BaseModel): + id: str + + function: RequiredActionSubmitToolOutputsToolCallFunction + + index: int + + type: Optional[Literal["function"]] = None + + +class RequiredActionSubmitToolOutputs(BaseModel): + tool_calls: List[RequiredActionSubmitToolOutputsToolCall] + + +class RequiredAction(BaseModel): + submit_tool_outputs: RequiredActionSubmitToolOutputs + + type: Optional[Literal["submit_tool_outputs"]] = None class ToolFunctionParameters(BaseModel): type: str + additional_properties: Optional[bool] = FieldInfo(alias="additionalProperties", default=None) + properties: Optional[Dict[str, object]] = None required: Optional[List[str]] = None @@ -69,6 +111,8 @@ class Run(BaseModel): model: Optional[Literal["gpt-4o"]] = None + required_action: Optional[RequiredAction] = None + tools: Optional[List[Tool]] = None usage: Optional[Usage] = None diff --git a/src/agility/types/threads/run_create_params.py b/src/agility/types/threads/run_create_params.py index bb06dcb..90c6b93 100644 --- a/src/agility/types/threads/run_create_params.py +++ b/src/agility/types/threads/run_create_params.py @@ -3,7 +3,9 @@ from __future__ import annotations from typing import Dict, List, Iterable, Optional -from typing_extensions import Literal, Required, TypedDict +from typing_extensions import Literal, Required, Annotated, TypedDict + +from ..._utils import PropertyInfo __all__ = [ "RunCreateParams", @@ -48,6 +50,8 @@ class AdditionalMessage(TypedDict, total=False): class ToolFunctionParameters(TypedDict, total=False): type: Required[str] + additional_properties: Annotated[bool, PropertyInfo(alias="additionalProperties")] + properties: Dict[str, object] required: Optional[List[str]] diff --git a/src/agility/types/threads/run_stream_params.py b/src/agility/types/threads/run_stream_params.py index 1cc770d..3cbb2e3 100644 --- a/src/agility/types/threads/run_stream_params.py +++ b/src/agility/types/threads/run_stream_params.py @@ -3,7 +3,9 @@ from __future__ import annotations from typing import Dict, List, Iterable, Optional -from typing_extensions import Literal, Required, TypedDict +from typing_extensions import Literal, Required, Annotated, TypedDict + +from ..._utils import PropertyInfo __all__ = [ "RunStreamParams", @@ -48,6 +50,8 @@ class AdditionalMessage(TypedDict, total=False): class ToolFunctionParameters(TypedDict, total=False): type: Required[str] + additional_properties: Annotated[bool, PropertyInfo(alias="additionalProperties")] + properties: Dict[str, object] required: Optional[List[str]] diff --git a/tests/api_resources/test_assistants.py b/tests/api_resources/test_assistants.py index 6316714..f529674 100644 --- a/tests/api_resources/test_assistants.py +++ b/tests/api_resources/test_assistants.py @@ -44,6 +44,7 @@ def test_method_create_with_all_params(self, client: Agility) -> None: "name": "name", "parameters": { "type": "type", + "additional_properties": True, "properties": {"foo": "bar"}, "required": ["string", "string", "string"], }, @@ -57,6 +58,7 @@ def test_method_create_with_all_params(self, client: Agility) -> None: "name": "name", "parameters": { "type": "type", + "additional_properties": True, "properties": {"foo": "bar"}, "required": ["string", "string", "string"], }, @@ -70,6 +72,7 @@ def test_method_create_with_all_params(self, client: Agility) -> None: "name": "name", "parameters": { "type": "type", + "additional_properties": True, "properties": {"foo": "bar"}, "required": ["string", "string", "string"], }, @@ -175,6 +178,7 @@ def test_method_update_with_all_params(self, client: Agility) -> None: "name": "name", "parameters": { "type": "type", + "additional_properties": True, "properties": {"foo": "bar"}, "required": ["string", "string", "string"], }, @@ -188,6 +192,7 @@ def test_method_update_with_all_params(self, client: Agility) -> None: "name": "name", "parameters": { "type": "type", + "additional_properties": True, "properties": {"foo": "bar"}, "required": ["string", "string", "string"], }, @@ -201,6 +206,7 @@ def test_method_update_with_all_params(self, client: Agility) -> None: "name": "name", "parameters": { "type": "type", + "additional_properties": True, "properties": {"foo": "bar"}, "required": ["string", "string", "string"], }, @@ -354,6 +360,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgility) - "name": "name", "parameters": { "type": "type", + "additional_properties": True, "properties": {"foo": "bar"}, "required": ["string", "string", "string"], }, @@ -367,6 +374,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgility) - "name": "name", "parameters": { "type": "type", + "additional_properties": True, "properties": {"foo": "bar"}, "required": ["string", "string", "string"], }, @@ -380,6 +388,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgility) - "name": "name", "parameters": { "type": "type", + "additional_properties": True, "properties": {"foo": "bar"}, "required": ["string", "string", "string"], }, @@ -485,6 +494,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncAgility) - "name": "name", "parameters": { "type": "type", + "additional_properties": True, "properties": {"foo": "bar"}, "required": ["string", "string", "string"], }, @@ -498,6 +508,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncAgility) - "name": "name", "parameters": { "type": "type", + "additional_properties": True, "properties": {"foo": "bar"}, "required": ["string", "string", "string"], }, @@ -511,6 +522,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncAgility) - "name": "name", "parameters": { "type": "type", + "additional_properties": True, "properties": {"foo": "bar"}, "required": ["string", "string", "string"], }, diff --git a/tests/api_resources/threads/test_runs.py b/tests/api_resources/threads/test_runs.py index b1536ee..c0aed0f 100644 --- a/tests/api_resources/threads/test_runs.py +++ b/tests/api_resources/threads/test_runs.py @@ -61,6 +61,7 @@ def test_method_create_with_all_params(self, client: Agility) -> None: "name": "name", "parameters": { "type": "type", + "additional_properties": True, "properties": {"foo": "bar"}, "required": ["string", "string", "string"], }, @@ -74,6 +75,7 @@ def test_method_create_with_all_params(self, client: Agility) -> None: "name": "name", "parameters": { "type": "type", + "additional_properties": True, "properties": {"foo": "bar"}, "required": ["string", "string", "string"], }, @@ -87,6 +89,7 @@ def test_method_create_with_all_params(self, client: Agility) -> None: "name": "name", "parameters": { "type": "type", + "additional_properties": True, "properties": {"foo": "bar"}, "required": ["string", "string", "string"], }, @@ -272,6 +275,7 @@ def test_method_stream_with_all_params(self, client: Agility) -> None: "name": "name", "parameters": { "type": "type", + "additional_properties": True, "properties": {"foo": "bar"}, "required": ["string", "string", "string"], }, @@ -285,6 +289,7 @@ def test_method_stream_with_all_params(self, client: Agility) -> None: "name": "name", "parameters": { "type": "type", + "additional_properties": True, "properties": {"foo": "bar"}, "required": ["string", "string", "string"], }, @@ -298,6 +303,7 @@ def test_method_stream_with_all_params(self, client: Agility) -> None: "name": "name", "parameters": { "type": "type", + "additional_properties": True, "properties": {"foo": "bar"}, "required": ["string", "string", "string"], }, @@ -391,6 +397,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgility) - "name": "name", "parameters": { "type": "type", + "additional_properties": True, "properties": {"foo": "bar"}, "required": ["string", "string", "string"], }, @@ -404,6 +411,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgility) - "name": "name", "parameters": { "type": "type", + "additional_properties": True, "properties": {"foo": "bar"}, "required": ["string", "string", "string"], }, @@ -417,6 +425,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgility) - "name": "name", "parameters": { "type": "type", + "additional_properties": True, "properties": {"foo": "bar"}, "required": ["string", "string", "string"], }, @@ -602,6 +611,7 @@ async def test_method_stream_with_all_params(self, async_client: AsyncAgility) - "name": "name", "parameters": { "type": "type", + "additional_properties": True, "properties": {"foo": "bar"}, "required": ["string", "string", "string"], }, @@ -615,6 +625,7 @@ async def test_method_stream_with_all_params(self, async_client: AsyncAgility) - "name": "name", "parameters": { "type": "type", + "additional_properties": True, "properties": {"foo": "bar"}, "required": ["string", "string", "string"], }, @@ -628,6 +639,7 @@ async def test_method_stream_with_all_params(self, async_client: AsyncAgility) - "name": "name", "parameters": { "type": "type", + "additional_properties": True, "properties": {"foo": "bar"}, "required": ["string", "string", "string"], }, diff --git a/tests/test_models.py b/tests/test_models.py index 94ffc80..4ba3148 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 45dd0c8788a5bb53da525824545584c00c9b6180 Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Thu, 17 Oct 2024 18:52:21 +0000 Subject: [PATCH 07/77] feat(api): api update --- .stats.yml | 2 +- api.md | 1 + src/agility/resources/threads/runs.py | 94 +++++++++++++++- src/agility/types/threads/__init__.py | 1 + .../threads/run_submit_tool_outputs_params.py | 20 ++++ tests/api_resources/threads/test_runs.py | 106 ++++++++++++++++++ 6 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 src/agility/types/threads/run_submit_tool_outputs_params.py diff --git a/.stats.yml b/.stats.yml index 813d1c9..c654935 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ -configured_endpoints: 38 +configured_endpoints: 39 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-855f9ee2331d7d0a691a87774cb55d84cb3099d6e116dfee6c4a7085e7719291.yml diff --git a/api.md b/api.md index 06687b2..0dd5f50 100644 --- a/api.md +++ b/api.md @@ -161,3 +161,4 @@ Methods: - client.threads.runs.retrieve(run_id, \*, thread_id) -> Run - client.threads.runs.delete(run_id, \*, thread_id) -> None - client.threads.runs.stream(thread_id, \*\*params) -> object +- client.threads.runs.submit_tool_outputs(run_id, \*, thread_id, \*\*params) -> Run diff --git a/src/agility/resources/threads/runs.py b/src/agility/resources/threads/runs.py index aaf4330..58051cb 100644 --- a/src/agility/resources/threads/runs.py +++ b/src/agility/resources/threads/runs.py @@ -21,7 +21,7 @@ async_to_streamed_response_wrapper, ) from ..._base_client import make_request_options -from ...types.threads import run_create_params, run_stream_params +from ...types.threads import run_create_params, run_stream_params, run_submit_tool_outputs_params from ...types.threads.run import Run __all__ = ["RunsResource", "AsyncRunsResource"] @@ -224,6 +224,46 @@ def stream( cast_to=object, ) + def submit_tool_outputs( + self, + run_id: str, + *, + thread_id: str, + body: Iterable[run_submit_tool_outputs_params.Body], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Run: + """Submit the outputs from the tool calls once they're all completed. + + The run must + have status: "requires_action" and required_action.type is submit_tool_outputs + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + if not run_id: + raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") + return self._post( + f"/api/threads/{thread_id}/runs/{run_id}/submit_tool_outputs", + body=maybe_transform(body, Iterable[run_submit_tool_outputs_params.Body]), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Run, + ) + class AsyncRunsResource(AsyncAPIResource): @cached_property @@ -422,6 +462,46 @@ async def stream( cast_to=object, ) + async def submit_tool_outputs( + self, + run_id: str, + *, + thread_id: str, + body: Iterable[run_submit_tool_outputs_params.Body], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Run: + """Submit the outputs from the tool calls once they're all completed. + + The run must + have status: "requires_action" and required_action.type is submit_tool_outputs + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + if not run_id: + raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") + return await self._post( + f"/api/threads/{thread_id}/runs/{run_id}/submit_tool_outputs", + body=await async_maybe_transform(body, Iterable[run_submit_tool_outputs_params.Body]), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Run, + ) + class RunsResourceWithRawResponse: def __init__(self, runs: RunsResource) -> None: @@ -439,6 +519,9 @@ def __init__(self, runs: RunsResource) -> None: self.stream = to_raw_response_wrapper( runs.stream, ) + self.submit_tool_outputs = to_raw_response_wrapper( + runs.submit_tool_outputs, + ) class AsyncRunsResourceWithRawResponse: @@ -457,6 +540,9 @@ def __init__(self, runs: AsyncRunsResource) -> None: self.stream = async_to_raw_response_wrapper( runs.stream, ) + self.submit_tool_outputs = async_to_raw_response_wrapper( + runs.submit_tool_outputs, + ) class RunsResourceWithStreamingResponse: @@ -475,6 +561,9 @@ def __init__(self, runs: RunsResource) -> None: self.stream = to_streamed_response_wrapper( runs.stream, ) + self.submit_tool_outputs = to_streamed_response_wrapper( + runs.submit_tool_outputs, + ) class AsyncRunsResourceWithStreamingResponse: @@ -493,3 +582,6 @@ def __init__(self, runs: AsyncRunsResource) -> None: self.stream = async_to_streamed_response_wrapper( runs.stream, ) + self.submit_tool_outputs = async_to_streamed_response_wrapper( + runs.submit_tool_outputs, + ) diff --git a/src/agility/types/threads/__init__.py b/src/agility/types/threads/__init__.py index 6b46983..51356af 100644 --- a/src/agility/types/threads/__init__.py +++ b/src/agility/types/threads/__init__.py @@ -9,3 +9,4 @@ from .message_list_params import MessageListParams as MessageListParams from .message_create_params import MessageCreateParams as MessageCreateParams from .message_list_response import MessageListResponse as MessageListResponse +from .run_submit_tool_outputs_params import RunSubmitToolOutputsParams as RunSubmitToolOutputsParams diff --git a/src/agility/types/threads/run_submit_tool_outputs_params.py b/src/agility/types/threads/run_submit_tool_outputs_params.py new file mode 100644 index 0000000..094cc4a --- /dev/null +++ b/src/agility/types/threads/run_submit_tool_outputs_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 import Iterable, Optional +from typing_extensions import Required, TypedDict + +__all__ = ["RunSubmitToolOutputsParams", "Body"] + + +class RunSubmitToolOutputsParams(TypedDict, total=False): + thread_id: Required[str] + + body: Required[Iterable[Body]] + + +class Body(TypedDict, total=False): + output: Optional[str] + + tool_call_id: Optional[str] diff --git a/tests/api_resources/threads/test_runs.py b/tests/api_resources/threads/test_runs.py index c0aed0f..1fb019d 100644 --- a/tests/api_resources/threads/test_runs.py +++ b/tests/api_resources/threads/test_runs.py @@ -349,6 +349,59 @@ def test_path_params_stream(self, client: Agility) -> None: assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) + @parametrize + def test_method_submit_tool_outputs(self, client: Agility) -> None: + run = client.threads.runs.submit_tool_outputs( + run_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + body=[{}, {}, {}], + ) + assert_matches_type(Run, run, path=["response"]) + + @parametrize + def test_raw_response_submit_tool_outputs(self, client: Agility) -> None: + response = client.threads.runs.with_raw_response.submit_tool_outputs( + run_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + body=[{}, {}, {}], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + run = response.parse() + assert_matches_type(Run, run, path=["response"]) + + @parametrize + def test_streaming_response_submit_tool_outputs(self, client: Agility) -> None: + with client.threads.runs.with_streaming_response.submit_tool_outputs( + run_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + body=[{}, {}, {}], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + run = response.parse() + assert_matches_type(Run, run, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_submit_tool_outputs(self, client: Agility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `thread_id` but received ''"): + client.threads.runs.with_raw_response.submit_tool_outputs( + run_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="", + body=[{}, {}, {}], + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `run_id` but received ''"): + client.threads.runs.with_raw_response.submit_tool_outputs( + run_id="", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + body=[{}, {}, {}], + ) + class TestAsyncRuns: parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) @@ -684,3 +737,56 @@ async def test_path_params_stream(self, async_client: AsyncAgility) -> None: thread_id="", assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) + + @parametrize + async def test_method_submit_tool_outputs(self, async_client: AsyncAgility) -> None: + run = await async_client.threads.runs.submit_tool_outputs( + run_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + body=[{}, {}, {}], + ) + assert_matches_type(Run, run, path=["response"]) + + @parametrize + async def test_raw_response_submit_tool_outputs(self, async_client: AsyncAgility) -> None: + response = await async_client.threads.runs.with_raw_response.submit_tool_outputs( + run_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + body=[{}, {}, {}], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + run = await response.parse() + assert_matches_type(Run, run, path=["response"]) + + @parametrize + async def test_streaming_response_submit_tool_outputs(self, async_client: AsyncAgility) -> None: + async with async_client.threads.runs.with_streaming_response.submit_tool_outputs( + run_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + body=[{}, {}, {}], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + run = await response.parse() + assert_matches_type(Run, run, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_submit_tool_outputs(self, async_client: AsyncAgility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `thread_id` but received ''"): + await async_client.threads.runs.with_raw_response.submit_tool_outputs( + run_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + thread_id="", + body=[{}, {}, {}], + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `run_id` but received ''"): + await async_client.threads.runs.with_raw_response.submit_tool_outputs( + run_id="", + thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + body=[{}, {}, {}], + ) From 3cebe31f846a92d6a0214bb114447af181f54f53 Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Fri, 1 Nov 2024 17:16:51 +0000 Subject: [PATCH 08/77] feat(api): api update --- .stats.yml | 4 +- README.md | 8 +- api.md | 5 +- pyproject.toml | 8 +- requirements-dev.lock | 25 +- requirements.lock | 8 +- src/agility/_base_client.py | 2 +- src/agility/_compat.py | 2 +- src/agility/_models.py | 10 +- src/agility/_types.py | 6 +- .../resources/assistants/assistants.py | 67 ++++- .../knowledge_bases/sources/sources.py | 28 +- src/agility/resources/threads/runs.py | 102 +------ src/agility/types/__init__.py | 1 + src/agility/types/assistant.py | 10 +- src/agility/types/assistant_create_params.py | 45 +-- src/agility/types/assistant_update_params.py | 45 +-- src/agility/types/assistant_with_config.py | 45 +-- src/agility/types/health_check_response.py | 1 - .../types/knowledge_base_create_params.py | 34 +++ .../types/knowledge_base_update_params.py | 34 +++ .../types/knowledge_base_with_config.py | 34 +++ .../knowledge_bases/source_create_params.py | 2 + .../knowledge_bases/source_update_params.py | 2 + src/agility/types/threads/__init__.py | 1 - src/agility/types/threads/run.py | 81 +---- .../types/threads/run_create_params.py | 48 +-- .../types/threads/run_stream_params.py | 48 +-- .../threads/run_submit_tool_outputs_params.py | 20 -- .../knowledge_bases/test_sources.py | 4 + tests/api_resources/test_assistants.py | 201 ++----------- tests/api_resources/threads/test_runs.py | 282 ------------------ tests/conftest.py | 14 +- tests/test_client.py | 21 +- 34 files changed, 315 insertions(+), 933 deletions(-) delete mode 100644 src/agility/types/threads/run_submit_tool_outputs_params.py diff --git a/.stats.yml b/.stats.yml index c654935..d06e34a 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ -configured_endpoints: 39 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-855f9ee2331d7d0a691a87774cb55d84cb3099d6e116dfee6c4a7085e7719291.yml +configured_endpoints: 38 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-36e7c2cdfead9cc1628d9e75a9be97a5580130287e8610c5bceac14770743512.yml diff --git a/README.md b/README.md index beff70f..d74c442 100644 --- a/README.md +++ b/README.md @@ -31,12 +31,12 @@ from agility import Agility client = Agility() -assistant_with_config = client.assistants.create( +assistant = client.assistants.create( description="description", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", name="name", ) -print(assistant_with_config.id) +print(assistant.id) ``` While you can provide an `api_key` keyword argument, @@ -56,12 +56,12 @@ client = AsyncAgility() async def main() -> None: - assistant_with_config = await client.assistants.create( + assistant = await client.assistants.create( description="description", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", name="name", ) - print(assistant_with_config.id) + print(assistant.id) asyncio.run(main()) diff --git a/api.md b/api.md index 0dd5f50..e2533b5 100644 --- a/api.md +++ b/api.md @@ -3,12 +3,12 @@ Types: ```python -from agility.types import AssistantWithConfig, AssistantListResponse +from agility.types import Assistant, AssistantWithConfig, AssistantListResponse ``` Methods: -- client.assistants.create(\*\*params) -> AssistantWithConfig +- client.assistants.create(\*\*params) -> Assistant - client.assistants.retrieve(assistant_id) -> AssistantWithConfig - client.assistants.update(assistant_id, \*\*params) -> AssistantWithConfig - client.assistants.list(\*\*params) -> AssistantListResponse @@ -161,4 +161,3 @@ Methods: - client.threads.runs.retrieve(run_id, \*, thread_id) -> Run - client.threads.runs.delete(run_id, \*, thread_id) -> None - client.threads.runs.stream(thread_id, \*\*params) -> object -- client.threads.runs.submit_tool_outputs(run_id, \*, thread_id, \*\*params) -> Run diff --git a/pyproject.toml b/pyproject.toml index c5ec7ac..915150f 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/stainless-sdks/agility-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 0795d4d..c626f3c 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -16,8 +16,6 @@ anyio==4.4.0 # via httpx 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 agility -exceptiongroup==1.1.3 +exceptiongroup==1.2.2 # via anyio + # via pytest filelock==3.12.4 # via virtualenv h11==0.14.0 @@ -49,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 @@ -60,27 +59,25 @@ 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 agility -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 # 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 @@ -90,10 +87,10 @@ sniffio==1.3.0 # via anyio # via httpx 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 agility # via anyio # via mypy diff --git a/requirements.lock b/requirements.lock index e3e4e8b..0c055e5 100644 --- a/requirements.lock +++ b/requirements.lock @@ -19,7 +19,7 @@ certifi==2023.7.22 # via httpx distro==1.8.0 # via agility -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 agility -pydantic-core==2.18.2 +pydantic-core==2.23.4 # via pydantic sniffio==1.3.0 # via agility # via anyio # via httpx -typing-extensions==4.8.0 +typing-extensions==4.12.2 # via agility # via anyio # via pydantic diff --git a/src/agility/_base_client.py b/src/agility/_base_client.py index f00254b..62e01ed 100644 --- a/src/agility/_base_client.py +++ b/src/agility/_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/agility/_compat.py b/src/agility/_compat.py index 162a6fb..d89920d 100644 --- a/src/agility/_compat.py +++ b/src/agility/_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/agility/_models.py b/src/agility/_models.py index d386eaa..42551b7 100644 --- a/src/agility/_models.py +++ b/src/agility/_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/agility/_types.py b/src/agility/_types.py index 45f69c1..ccfc20c 100644 --- a/src/agility/_types.py +++ b/src/agility/_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/src/agility/resources/assistants/assistants.py b/src/agility/resources/assistants/assistants.py index 14d5f6d..023e47c 100644 --- a/src/agility/resources/assistants/assistants.py +++ b/src/agility/resources/assistants/assistants.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Iterable, Optional +from typing import List, Optional from typing_extensions import Literal import httpx @@ -30,6 +30,7 @@ AsyncAccessKeysResourceWithStreamingResponse, ) from ..._base_client import make_request_options +from ...types.assistant import Assistant from ...types.assistant_with_config import AssistantWithConfig from ...types.assistant_list_response import AssistantListResponse @@ -68,18 +69,27 @@ def create( name: str, instructions: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, - tools: Optional[Iterable[assistant_create_params.Tool]] | NotGiven = NOT_GIVEN, + suggested_questions: List[str] | NotGiven = NOT_GIVEN, + url_slug: Optional[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, - ) -> AssistantWithConfig: + ) -> Assistant: """ Create a new assistant. Args: + description: The description of the assistant + + name: The name of the assistant + + suggested_questions: A list of suggested questions that can be asked to the assistant + + url_slug: Optional URL suffix - unique identifier for the assistant's endpoint + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -97,14 +107,15 @@ def create( "name": name, "instructions": instructions, "model": model, - "tools": tools, + "suggested_questions": suggested_questions, + "url_slug": url_slug, }, assistant_create_params.AssistantCreateParams, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=AssistantWithConfig, + cast_to=Assistant, ) def retrieve( @@ -150,7 +161,8 @@ def update( name: str, instructions: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, - tools: Optional[Iterable[assistant_update_params.Tool]] | NotGiven = NOT_GIVEN, + suggested_questions: List[str] | NotGiven = NOT_GIVEN, + url_slug: Optional[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, @@ -162,6 +174,14 @@ def update( Update an assistant. Args: + description: The description of the assistant + + name: The name of the assistant + + suggested_questions: A list of suggested questions that can be asked to the assistant + + url_slug: Optional URL suffix - unique identifier for the assistant's endpoint + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -182,7 +202,8 @@ def update( "name": name, "instructions": instructions, "model": model, - "tools": tools, + "suggested_questions": suggested_questions, + "url_slug": url_slug, }, assistant_update_params.AssistantUpdateParams, ), @@ -301,18 +322,27 @@ async def create( name: str, instructions: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, - tools: Optional[Iterable[assistant_create_params.Tool]] | NotGiven = NOT_GIVEN, + suggested_questions: List[str] | NotGiven = NOT_GIVEN, + url_slug: Optional[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, - ) -> AssistantWithConfig: + ) -> Assistant: """ Create a new assistant. Args: + description: The description of the assistant + + name: The name of the assistant + + suggested_questions: A list of suggested questions that can be asked to the assistant + + url_slug: Optional URL suffix - unique identifier for the assistant's endpoint + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -330,14 +360,15 @@ async def create( "name": name, "instructions": instructions, "model": model, - "tools": tools, + "suggested_questions": suggested_questions, + "url_slug": url_slug, }, assistant_create_params.AssistantCreateParams, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=AssistantWithConfig, + cast_to=Assistant, ) async def retrieve( @@ -383,7 +414,8 @@ async def update( name: str, instructions: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, - tools: Optional[Iterable[assistant_update_params.Tool]] | NotGiven = NOT_GIVEN, + suggested_questions: List[str] | NotGiven = NOT_GIVEN, + url_slug: Optional[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, @@ -395,6 +427,14 @@ async def update( Update an assistant. Args: + description: The description of the assistant + + name: The name of the assistant + + suggested_questions: A list of suggested questions that can be asked to the assistant + + url_slug: Optional URL suffix - unique identifier for the assistant's endpoint + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -415,7 +455,8 @@ async def update( "name": name, "instructions": instructions, "model": model, - "tools": tools, + "suggested_questions": suggested_questions, + "url_slug": url_slug, }, assistant_update_params.AssistantUpdateParams, ), diff --git a/src/agility/resources/knowledge_bases/sources/sources.py b/src/agility/resources/knowledge_bases/sources/sources.py index 73d47f9..b509b33 100644 --- a/src/agility/resources/knowledge_bases/sources/sources.py +++ b/src/agility/resources/knowledge_bases/sources/sources.py @@ -66,6 +66,7 @@ def create( name: str, source_params: source_create_params.SourceParams, source_schedule: source_create_params.SourceSchedule, + sync: 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, @@ -103,7 +104,11 @@ def create( source_create_params.SourceCreateParams, ), 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({"sync": sync}, source_create_params.SourceCreateParams), ), cast_to=Source, ) @@ -153,6 +158,7 @@ def update( name: str, source_params: source_update_params.SourceParams, source_schedule: source_update_params.SourceSchedule, + sync: 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, @@ -192,7 +198,11 @@ def update( source_update_params.SourceUpdateParams, ), 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({"sync": sync}, source_update_params.SourceUpdateParams), ), cast_to=Source, ) @@ -384,6 +394,7 @@ async def create( name: str, source_params: source_create_params.SourceParams, source_schedule: source_create_params.SourceSchedule, + sync: 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, @@ -421,7 +432,11 @@ async def create( source_create_params.SourceCreateParams, ), 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({"sync": sync}, source_create_params.SourceCreateParams), ), cast_to=Source, ) @@ -471,6 +486,7 @@ async def update( name: str, source_params: source_update_params.SourceParams, source_schedule: source_update_params.SourceSchedule, + sync: 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, @@ -510,7 +526,11 @@ async def update( source_update_params.SourceUpdateParams, ), 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({"sync": sync}, source_update_params.SourceUpdateParams), ), cast_to=Source, ) diff --git a/src/agility/resources/threads/runs.py b/src/agility/resources/threads/runs.py index 58051cb..f76a5f7 100644 --- a/src/agility/resources/threads/runs.py +++ b/src/agility/resources/threads/runs.py @@ -21,7 +21,7 @@ async_to_streamed_response_wrapper, ) from ..._base_client import make_request_options -from ...types.threads import run_create_params, run_stream_params, run_submit_tool_outputs_params +from ...types.threads import run_create_params, run_stream_params from ...types.threads.run import Run __all__ = ["RunsResource", "AsyncRunsResource"] @@ -57,7 +57,6 @@ def create( instructions: Optional[str] | NotGiven = NOT_GIVEN, knowledge_base_id: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, - tools: Optional[Iterable[run_create_params.Tool]] | 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, @@ -89,7 +88,6 @@ def create( "instructions": instructions, "knowledge_base_id": knowledge_base_id, "model": model, - "tools": tools, }, run_create_params.RunCreateParams, ), @@ -182,7 +180,6 @@ def stream( instructions: Optional[str] | NotGiven = NOT_GIVEN, knowledge_base_id: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, - tools: Optional[Iterable[run_stream_params.Tool]] | 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, @@ -214,7 +211,6 @@ def stream( "instructions": instructions, "knowledge_base_id": knowledge_base_id, "model": model, - "tools": tools, }, run_stream_params.RunStreamParams, ), @@ -224,46 +220,6 @@ def stream( cast_to=object, ) - def submit_tool_outputs( - self, - run_id: str, - *, - thread_id: str, - body: Iterable[run_submit_tool_outputs_params.Body], - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Run: - """Submit the outputs from the tool calls once they're all completed. - - The run must - have status: "requires_action" and required_action.type is submit_tool_outputs - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not thread_id: - raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") - if not run_id: - raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") - return self._post( - f"/api/threads/{thread_id}/runs/{run_id}/submit_tool_outputs", - body=maybe_transform(body, Iterable[run_submit_tool_outputs_params.Body]), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=Run, - ) - class AsyncRunsResource(AsyncAPIResource): @cached_property @@ -295,7 +251,6 @@ async def create( instructions: Optional[str] | NotGiven = NOT_GIVEN, knowledge_base_id: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, - tools: Optional[Iterable[run_create_params.Tool]] | 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, @@ -327,7 +282,6 @@ async def create( "instructions": instructions, "knowledge_base_id": knowledge_base_id, "model": model, - "tools": tools, }, run_create_params.RunCreateParams, ), @@ -420,7 +374,6 @@ async def stream( instructions: Optional[str] | NotGiven = NOT_GIVEN, knowledge_base_id: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, - tools: Optional[Iterable[run_stream_params.Tool]] | 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, @@ -452,7 +405,6 @@ async def stream( "instructions": instructions, "knowledge_base_id": knowledge_base_id, "model": model, - "tools": tools, }, run_stream_params.RunStreamParams, ), @@ -462,46 +414,6 @@ async def stream( cast_to=object, ) - async def submit_tool_outputs( - self, - run_id: str, - *, - thread_id: str, - body: Iterable[run_submit_tool_outputs_params.Body], - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Run: - """Submit the outputs from the tool calls once they're all completed. - - The run must - have status: "requires_action" and required_action.type is submit_tool_outputs - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not thread_id: - raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") - if not run_id: - raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") - return await self._post( - f"/api/threads/{thread_id}/runs/{run_id}/submit_tool_outputs", - body=await async_maybe_transform(body, Iterable[run_submit_tool_outputs_params.Body]), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=Run, - ) - class RunsResourceWithRawResponse: def __init__(self, runs: RunsResource) -> None: @@ -519,9 +431,6 @@ def __init__(self, runs: RunsResource) -> None: self.stream = to_raw_response_wrapper( runs.stream, ) - self.submit_tool_outputs = to_raw_response_wrapper( - runs.submit_tool_outputs, - ) class AsyncRunsResourceWithRawResponse: @@ -540,9 +449,6 @@ def __init__(self, runs: AsyncRunsResource) -> None: self.stream = async_to_raw_response_wrapper( runs.stream, ) - self.submit_tool_outputs = async_to_raw_response_wrapper( - runs.submit_tool_outputs, - ) class RunsResourceWithStreamingResponse: @@ -561,9 +467,6 @@ def __init__(self, runs: RunsResource) -> None: self.stream = to_streamed_response_wrapper( runs.stream, ) - self.submit_tool_outputs = to_streamed_response_wrapper( - runs.submit_tool_outputs, - ) class AsyncRunsResourceWithStreamingResponse: @@ -582,6 +485,3 @@ def __init__(self, runs: AsyncRunsResource) -> None: self.stream = async_to_streamed_response_wrapper( runs.stream, ) - self.submit_tool_outputs = async_to_streamed_response_wrapper( - runs.submit_tool_outputs, - ) diff --git a/src/agility/types/__init__.py b/src/agility/types/__init__.py index 2124b90..791d45a 100644 --- a/src/agility/types/__init__.py +++ b/src/agility/types/__init__.py @@ -4,6 +4,7 @@ from .user import User as User from .thread import Thread as Thread +from .assistant import Assistant as Assistant from .thread_list_params import ThreadListParams as ThreadListParams from .thread_list_response import ThreadListResponse as ThreadListResponse from .assistant_list_params import AssistantListParams as AssistantListParams diff --git a/src/agility/types/assistant.py b/src/agility/types/assistant.py index 50b13df..52bf49d 100644 --- a/src/agility/types/assistant.py +++ b/src/agility/types/assistant.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional +from typing import List, Optional from datetime import datetime from .._models import BaseModel @@ -16,7 +16,15 @@ class Assistant(BaseModel): deleted_at: Optional[datetime] = None description: str + """The description of the assistant""" name: str + """The name of the assistant""" updated_at: datetime + + suggested_questions: Optional[List[str]] = None + """A list of suggested questions that can be asked to the assistant""" + + url_slug: Optional[str] = None + """Optional URL suffix - unique identifier for the assistant's endpoint""" diff --git a/src/agility/types/assistant_create_params.py b/src/agility/types/assistant_create_params.py index 8d31cde..f47e919 100644 --- a/src/agility/types/assistant_create_params.py +++ b/src/agility/types/assistant_create_params.py @@ -2,54 +2,27 @@ from __future__ import annotations -from typing import Dict, List, Iterable, Optional -from typing_extensions import Literal, Required, Annotated, TypedDict +from typing import List, Optional +from typing_extensions import Literal, Required, TypedDict -from .._utils import PropertyInfo - -__all__ = ["AssistantCreateParams", "Tool", "ToolFunction", "ToolFunctionParameters"] +__all__ = ["AssistantCreateParams"] class AssistantCreateParams(TypedDict, total=False): description: Required[str] + """The description of the assistant""" knowledge_base_id: Required[str] name: Required[str] + """The name of the assistant""" instructions: Optional[str] model: Optional[Literal["gpt-4o"]] - tools: Optional[Iterable[Tool]] - - -class ToolFunctionParameters(TypedDict, total=False): - type: Required[str] - - additional_properties: Annotated[bool, PropertyInfo(alias="additionalProperties")] - - properties: Dict[str, object] - - required: Optional[List[str]] - - -class ToolFunction(TypedDict, total=False): - description: Required[str] - """ - A description of what the function does, used by the model to choose when and - how to call the function. - """ - - name: Required[str] - """The name of the function to be called.""" - - parameters: Optional[ToolFunctionParameters] - - strict: bool - - -class Tool(TypedDict, total=False): - function: Required[ToolFunction] + suggested_questions: List[str] + """A list of suggested questions that can be asked to the assistant""" - type: Required[Literal["function"]] + url_slug: Optional[str] + """Optional URL suffix - unique identifier for the assistant's endpoint""" diff --git a/src/agility/types/assistant_update_params.py b/src/agility/types/assistant_update_params.py index 9704a06..53c5912 100644 --- a/src/agility/types/assistant_update_params.py +++ b/src/agility/types/assistant_update_params.py @@ -2,56 +2,29 @@ from __future__ import annotations -from typing import Dict, List, Iterable, Optional -from typing_extensions import Literal, Required, Annotated, TypedDict +from typing import List, Optional +from typing_extensions import Literal, Required, TypedDict -from .._utils import PropertyInfo - -__all__ = ["AssistantUpdateParams", "Tool", "ToolFunction", "ToolFunctionParameters"] +__all__ = ["AssistantUpdateParams"] class AssistantUpdateParams(TypedDict, total=False): id: Required[str] description: Required[str] + """The description of the assistant""" knowledge_base_id: Required[str] name: Required[str] + """The name of the assistant""" instructions: Optional[str] model: Optional[Literal["gpt-4o"]] - tools: Optional[Iterable[Tool]] - - -class ToolFunctionParameters(TypedDict, total=False): - type: Required[str] - - additional_properties: Annotated[bool, PropertyInfo(alias="additionalProperties")] - - properties: Dict[str, object] - - required: Optional[List[str]] - - -class ToolFunction(TypedDict, total=False): - description: Required[str] - """ - A description of what the function does, used by the model to choose when and - how to call the function. - """ - - name: Required[str] - """The name of the function to be called.""" - - parameters: Optional[ToolFunctionParameters] - - strict: bool - - -class Tool(TypedDict, total=False): - function: Required[ToolFunction] + suggested_questions: List[str] + """A list of suggested questions that can be asked to the assistant""" - type: Required[Literal["function"]] + url_slug: Optional[str] + """Optional URL suffix - unique identifier for the assistant's endpoint""" diff --git a/src/agility/types/assistant_with_config.py b/src/agility/types/assistant_with_config.py index 98f5029..7d903e9 100644 --- a/src/agility/types/assistant_with_config.py +++ b/src/agility/types/assistant_with_config.py @@ -1,45 +1,12 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Dict, List, Optional +from typing import List, Optional from datetime import datetime from typing_extensions import Literal -from pydantic import Field as FieldInfo - from .._models import BaseModel -__all__ = ["AssistantWithConfig", "Tool", "ToolFunction", "ToolFunctionParameters"] - - -class ToolFunctionParameters(BaseModel): - type: str - - additional_properties: Optional[bool] = FieldInfo(alias="additionalProperties", default=None) - - properties: Optional[Dict[str, object]] = None - - required: Optional[List[str]] = None - - -class ToolFunction(BaseModel): - description: str - """ - A description of what the function does, used by the model to choose when and - how to call the function. - """ - - name: str - """The name of the function to be called.""" - - parameters: Optional[ToolFunctionParameters] = None - - strict: Optional[bool] = None - - -class Tool(BaseModel): - function: ToolFunction - - type: Literal["function"] +__all__ = ["AssistantWithConfig"] class AssistantWithConfig(BaseModel): @@ -50,10 +17,12 @@ class AssistantWithConfig(BaseModel): deleted_at: Optional[datetime] = None description: str + """The description of the assistant""" knowledge_base_id: str name: str + """The name of the assistant""" updated_at: datetime @@ -61,4 +30,8 @@ class AssistantWithConfig(BaseModel): model: Optional[Literal["gpt-4o"]] = None - tools: Optional[List[Tool]] = None + suggested_questions: Optional[List[str]] = None + """A list of suggested questions that can be asked to the assistant""" + + url_slug: Optional[str] = None + """Optional URL suffix - unique identifier for the assistant's endpoint""" diff --git a/src/agility/types/health_check_response.py b/src/agility/types/health_check_response.py index 5303b51..7a2655d 100644 --- a/src/agility/types/health_check_response.py +++ b/src/agility/types/health_check_response.py @@ -1,7 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - from .._models import BaseModel __all__ = ["HealthCheckResponse"] diff --git a/src/agility/types/knowledge_base_create_params.py b/src/agility/types/knowledge_base_create_params.py index 3ec2349..71ce009 100644 --- a/src/agility/types/knowledge_base_create_params.py +++ b/src/agility/types/knowledge_base_create_params.py @@ -13,10 +13,13 @@ "IngestionPipelineParamsCurateStepsRemoveExactDuplicatesParams", "IngestionPipelineParamsCurateStepsTagExactDuplicatesParams", "IngestionPipelineParamsCurateStepsPostpendContentParams", + "IngestionPipelineParamsCurateStepsRemoveEmbeddedImagesParams", "IngestionPipelineParamsCurateDocumentStore", "IngestionPipelineParamsTransform", "IngestionPipelineParamsTransformSteps", "IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params", + "IngestionPipelineParamsTransformStepsSemanticMergeSplitterV0Params", + "IngestionPipelineParamsTransformStepsNodeSummarizerV0Params", "IngestionPipelineParamsTransformStepsNoopParams", "IngestionPipelineParamsVectorStore", ] @@ -49,10 +52,15 @@ class IngestionPipelineParamsCurateStepsPostpendContentParams(TypedDict, total=F name: Literal["postpend_content.v0"] +class IngestionPipelineParamsCurateStepsRemoveEmbeddedImagesParams(TypedDict, total=False): + name: Literal["remove_embedded_images.v0"] + + IngestionPipelineParamsCurateSteps: TypeAlias = Union[ IngestionPipelineParamsCurateStepsRemoveExactDuplicatesParams, IngestionPipelineParamsCurateStepsTagExactDuplicatesParams, IngestionPipelineParamsCurateStepsPostpendContentParams, + IngestionPipelineParamsCurateStepsRemoveEmbeddedImagesParams, ] @@ -72,12 +80,38 @@ class IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params(Ty name: Literal["splitters.recursive_character.v0"] +class IngestionPipelineParamsTransformStepsSemanticMergeSplitterV0Params(TypedDict, total=False): + appending_threshold: float + + initial_threshold: float + + max_chunk_size: int + + merging_range: int + + merging_threshold: float + + name: Literal["splitters.semantic_merge.v0"] + + +class IngestionPipelineParamsTransformStepsNodeSummarizerV0Params(TypedDict, total=False): + expected_summary_tokens: int + + max_prompt_input_tokens: int + + model: str + + name: Literal["node_summarizer.v0"] + + class IngestionPipelineParamsTransformStepsNoopParams(TypedDict, total=False): name: Literal["noop"] IngestionPipelineParamsTransformSteps: TypeAlias = Union[ IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params, + IngestionPipelineParamsTransformStepsSemanticMergeSplitterV0Params, + IngestionPipelineParamsTransformStepsNodeSummarizerV0Params, IngestionPipelineParamsTransformStepsNoopParams, ] diff --git a/src/agility/types/knowledge_base_update_params.py b/src/agility/types/knowledge_base_update_params.py index 6c2339e..2582fc1 100644 --- a/src/agility/types/knowledge_base_update_params.py +++ b/src/agility/types/knowledge_base_update_params.py @@ -13,10 +13,13 @@ "IngestionPipelineParamsCurateStepsRemoveExactDuplicatesParams", "IngestionPipelineParamsCurateStepsTagExactDuplicatesParams", "IngestionPipelineParamsCurateStepsPostpendContentParams", + "IngestionPipelineParamsCurateStepsRemoveEmbeddedImagesParams", "IngestionPipelineParamsCurateDocumentStore", "IngestionPipelineParamsTransform", "IngestionPipelineParamsTransformSteps", "IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params", + "IngestionPipelineParamsTransformStepsSemanticMergeSplitterV0Params", + "IngestionPipelineParamsTransformStepsNodeSummarizerV0Params", "IngestionPipelineParamsTransformStepsNoopParams", "IngestionPipelineParamsVectorStore", ] @@ -49,10 +52,15 @@ class IngestionPipelineParamsCurateStepsPostpendContentParams(TypedDict, total=F name: Literal["postpend_content.v0"] +class IngestionPipelineParamsCurateStepsRemoveEmbeddedImagesParams(TypedDict, total=False): + name: Literal["remove_embedded_images.v0"] + + IngestionPipelineParamsCurateSteps: TypeAlias = Union[ IngestionPipelineParamsCurateStepsRemoveExactDuplicatesParams, IngestionPipelineParamsCurateStepsTagExactDuplicatesParams, IngestionPipelineParamsCurateStepsPostpendContentParams, + IngestionPipelineParamsCurateStepsRemoveEmbeddedImagesParams, ] @@ -72,12 +80,38 @@ class IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params(Ty name: Literal["splitters.recursive_character.v0"] +class IngestionPipelineParamsTransformStepsSemanticMergeSplitterV0Params(TypedDict, total=False): + appending_threshold: float + + initial_threshold: float + + max_chunk_size: int + + merging_range: int + + merging_threshold: float + + name: Literal["splitters.semantic_merge.v0"] + + +class IngestionPipelineParamsTransformStepsNodeSummarizerV0Params(TypedDict, total=False): + expected_summary_tokens: int + + max_prompt_input_tokens: int + + model: str + + name: Literal["node_summarizer.v0"] + + class IngestionPipelineParamsTransformStepsNoopParams(TypedDict, total=False): name: Literal["noop"] IngestionPipelineParamsTransformSteps: TypeAlias = Union[ IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params, + IngestionPipelineParamsTransformStepsSemanticMergeSplitterV0Params, + IngestionPipelineParamsTransformStepsNodeSummarizerV0Params, IngestionPipelineParamsTransformStepsNoopParams, ] diff --git a/src/agility/types/knowledge_base_with_config.py b/src/agility/types/knowledge_base_with_config.py index 6aa07b6..ae9f8d7 100644 --- a/src/agility/types/knowledge_base_with_config.py +++ b/src/agility/types/knowledge_base_with_config.py @@ -15,10 +15,13 @@ "IngestionPipelineParamsCurateStepsRemoveExactDuplicatesParams", "IngestionPipelineParamsCurateStepsTagExactDuplicatesParams", "IngestionPipelineParamsCurateStepsPostpendContentParams", + "IngestionPipelineParamsCurateStepsRemoveEmbeddedImagesParams", "IngestionPipelineParamsCurateDocumentStore", "IngestionPipelineParamsTransform", "IngestionPipelineParamsTransformSteps", "IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params", + "IngestionPipelineParamsTransformStepsSemanticMergeSplitterV0Params", + "IngestionPipelineParamsTransformStepsNodeSummarizerV0Params", "IngestionPipelineParamsTransformStepsNoopParams", "IngestionPipelineParamsVectorStore", ] @@ -39,11 +42,16 @@ class IngestionPipelineParamsCurateStepsPostpendContentParams(BaseModel): name: Optional[Literal["postpend_content.v0"]] = None +class IngestionPipelineParamsCurateStepsRemoveEmbeddedImagesParams(BaseModel): + name: Optional[Literal["remove_embedded_images.v0"]] = None + + IngestionPipelineParamsCurateSteps: TypeAlias = Annotated[ Union[ IngestionPipelineParamsCurateStepsRemoveExactDuplicatesParams, IngestionPipelineParamsCurateStepsTagExactDuplicatesParams, IngestionPipelineParamsCurateStepsPostpendContentParams, + IngestionPipelineParamsCurateStepsRemoveEmbeddedImagesParams, ], PropertyInfo(discriminator="name"), ] @@ -65,12 +73,38 @@ class IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params(Ba name: Optional[Literal["splitters.recursive_character.v0"]] = None +class IngestionPipelineParamsTransformStepsSemanticMergeSplitterV0Params(BaseModel): + appending_threshold: Optional[float] = None + + initial_threshold: Optional[float] = None + + max_chunk_size: Optional[int] = None + + merging_range: Optional[int] = None + + merging_threshold: Optional[float] = None + + name: Optional[Literal["splitters.semantic_merge.v0"]] = None + + +class IngestionPipelineParamsTransformStepsNodeSummarizerV0Params(BaseModel): + expected_summary_tokens: Optional[int] = None + + max_prompt_input_tokens: Optional[int] = None + + model: Optional[str] = None + + name: Optional[Literal["node_summarizer.v0"]] = None + + class IngestionPipelineParamsTransformStepsNoopParams(BaseModel): name: Optional[Literal["noop"]] = None IngestionPipelineParamsTransformSteps: TypeAlias = Union[ IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params, + IngestionPipelineParamsTransformStepsSemanticMergeSplitterV0Params, + IngestionPipelineParamsTransformStepsNodeSummarizerV0Params, IngestionPipelineParamsTransformStepsNoopParams, ] diff --git a/src/agility/types/knowledge_bases/source_create_params.py b/src/agility/types/knowledge_bases/source_create_params.py index ccdf6b5..0014862 100644 --- a/src/agility/types/knowledge_bases/source_create_params.py +++ b/src/agility/types/knowledge_bases/source_create_params.py @@ -26,6 +26,8 @@ class SourceCreateParams(TypedDict, total=False): source_schedule: Required[SourceSchedule] """Source schedule model.""" + sync: bool + class SourceParamsWebV0Params(TypedDict, total=False): urls: Required[List[str]] diff --git a/src/agility/types/knowledge_bases/source_update_params.py b/src/agility/types/knowledge_bases/source_update_params.py index eb5034b..bfbad51 100644 --- a/src/agility/types/knowledge_bases/source_update_params.py +++ b/src/agility/types/knowledge_bases/source_update_params.py @@ -28,6 +28,8 @@ class SourceUpdateParams(TypedDict, total=False): source_schedule: Required[SourceSchedule] """Source schedule model.""" + sync: bool + class SourceParamsWebV0Params(TypedDict, total=False): urls: Required[List[str]] diff --git a/src/agility/types/threads/__init__.py b/src/agility/types/threads/__init__.py index 51356af..6b46983 100644 --- a/src/agility/types/threads/__init__.py +++ b/src/agility/types/threads/__init__.py @@ -9,4 +9,3 @@ from .message_list_params import MessageListParams as MessageListParams from .message_create_params import MessageCreateParams as MessageCreateParams from .message_list_response import MessageListResponse as MessageListResponse -from .run_submit_tool_outputs_params import RunSubmitToolOutputsParams as RunSubmitToolOutputsParams diff --git a/src/agility/types/threads/run.py b/src/agility/types/threads/run.py index c483ff3..192adc5 100644 --- a/src/agility/types/threads/run.py +++ b/src/agility/types/threads/run.py @@ -1,83 +1,12 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Dict, List, Optional +from typing import Optional from datetime import datetime from typing_extensions import Literal -from pydantic import Field as FieldInfo - from ..._models import BaseModel -__all__ = [ - "Run", - "RequiredAction", - "RequiredActionSubmitToolOutputs", - "RequiredActionSubmitToolOutputsToolCall", - "RequiredActionSubmitToolOutputsToolCallFunction", - "Tool", - "ToolFunction", - "ToolFunctionParameters", - "Usage", -] - - -class RequiredActionSubmitToolOutputsToolCallFunction(BaseModel): - arguments: str - - name: str - - output: Optional[str] = None - - -class RequiredActionSubmitToolOutputsToolCall(BaseModel): - id: str - - function: RequiredActionSubmitToolOutputsToolCallFunction - - index: int - - type: Optional[Literal["function"]] = None - - -class RequiredActionSubmitToolOutputs(BaseModel): - tool_calls: List[RequiredActionSubmitToolOutputsToolCall] - - -class RequiredAction(BaseModel): - submit_tool_outputs: RequiredActionSubmitToolOutputs - - type: Optional[Literal["submit_tool_outputs"]] = None - - -class ToolFunctionParameters(BaseModel): - type: str - - additional_properties: Optional[bool] = FieldInfo(alias="additionalProperties", default=None) - - properties: Optional[Dict[str, object]] = None - - required: Optional[List[str]] = None - - -class ToolFunction(BaseModel): - description: str - """ - A description of what the function does, used by the model to choose when and - how to call the function. - """ - - name: str - """The name of the function to be called.""" - - parameters: Optional[ToolFunctionParameters] = None - - strict: Optional[bool] = None - - -class Tool(BaseModel): - function: ToolFunction - - type: Literal["function"] +__all__ = ["Run", "Usage"] class Usage(BaseModel): @@ -95,7 +24,7 @@ class Run(BaseModel): created_at: datetime - status: Literal["pending", "in_progress", "completed", "failed", "canceled", "expired", "requires_action"] + status: Literal["pending", "in_progress", "completed", "failed", "canceled", "expired"] thread_id: str @@ -111,8 +40,4 @@ class Run(BaseModel): model: Optional[Literal["gpt-4o"]] = None - required_action: Optional[RequiredAction] = None - - tools: Optional[List[Tool]] = None - usage: Optional[Usage] = None diff --git a/src/agility/types/threads/run_create_params.py b/src/agility/types/threads/run_create_params.py index 90c6b93..b42f8a7 100644 --- a/src/agility/types/threads/run_create_params.py +++ b/src/agility/types/threads/run_create_params.py @@ -2,19 +2,10 @@ from __future__ import annotations -from typing import Dict, List, Iterable, Optional -from typing_extensions import Literal, Required, Annotated, TypedDict +from typing import Iterable, Optional +from typing_extensions import Literal, Required, TypedDict -from ..._utils import PropertyInfo - -__all__ = [ - "RunCreateParams", - "AdditionalMessage", - "AdditionalMessageMetadata", - "Tool", - "ToolFunction", - "ToolFunctionParameters", -] +__all__ = ["RunCreateParams", "AdditionalMessage", "AdditionalMessageMetadata"] class RunCreateParams(TypedDict, total=False): @@ -30,8 +21,6 @@ class RunCreateParams(TypedDict, total=False): model: Optional[Literal["gpt-4o"]] - tools: Optional[Iterable[Tool]] - class AdditionalMessageMetadata(TypedDict, total=False): trustworthiness_score: Optional[float] @@ -45,34 +34,3 @@ class AdditionalMessage(TypedDict, total=False): role: Required[Literal["user", "assistant"]] thread_id: Required[str] - - -class ToolFunctionParameters(TypedDict, total=False): - type: Required[str] - - additional_properties: Annotated[bool, PropertyInfo(alias="additionalProperties")] - - properties: Dict[str, object] - - required: Optional[List[str]] - - -class ToolFunction(TypedDict, total=False): - description: Required[str] - """ - A description of what the function does, used by the model to choose when and - how to call the function. - """ - - name: Required[str] - """The name of the function to be called.""" - - parameters: Optional[ToolFunctionParameters] - - strict: bool - - -class Tool(TypedDict, total=False): - function: Required[ToolFunction] - - type: Required[Literal["function"]] diff --git a/src/agility/types/threads/run_stream_params.py b/src/agility/types/threads/run_stream_params.py index 3cbb2e3..804d30f 100644 --- a/src/agility/types/threads/run_stream_params.py +++ b/src/agility/types/threads/run_stream_params.py @@ -2,19 +2,10 @@ from __future__ import annotations -from typing import Dict, List, Iterable, Optional -from typing_extensions import Literal, Required, Annotated, TypedDict +from typing import Iterable, Optional +from typing_extensions import Literal, Required, TypedDict -from ..._utils import PropertyInfo - -__all__ = [ - "RunStreamParams", - "AdditionalMessage", - "AdditionalMessageMetadata", - "Tool", - "ToolFunction", - "ToolFunctionParameters", -] +__all__ = ["RunStreamParams", "AdditionalMessage", "AdditionalMessageMetadata"] class RunStreamParams(TypedDict, total=False): @@ -30,8 +21,6 @@ class RunStreamParams(TypedDict, total=False): model: Optional[Literal["gpt-4o"]] - tools: Optional[Iterable[Tool]] - class AdditionalMessageMetadata(TypedDict, total=False): trustworthiness_score: Optional[float] @@ -45,34 +34,3 @@ class AdditionalMessage(TypedDict, total=False): role: Required[Literal["user", "assistant"]] thread_id: Required[str] - - -class ToolFunctionParameters(TypedDict, total=False): - type: Required[str] - - additional_properties: Annotated[bool, PropertyInfo(alias="additionalProperties")] - - properties: Dict[str, object] - - required: Optional[List[str]] - - -class ToolFunction(TypedDict, total=False): - description: Required[str] - """ - A description of what the function does, used by the model to choose when and - how to call the function. - """ - - name: Required[str] - """The name of the function to be called.""" - - parameters: Optional[ToolFunctionParameters] - - strict: bool - - -class Tool(TypedDict, total=False): - function: Required[ToolFunction] - - type: Required[Literal["function"]] diff --git a/src/agility/types/threads/run_submit_tool_outputs_params.py b/src/agility/types/threads/run_submit_tool_outputs_params.py deleted file mode 100644 index 094cc4a..0000000 --- a/src/agility/types/threads/run_submit_tool_outputs_params.py +++ /dev/null @@ -1,20 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Iterable, Optional -from typing_extensions import Required, TypedDict - -__all__ = ["RunSubmitToolOutputsParams", "Body"] - - -class RunSubmitToolOutputsParams(TypedDict, total=False): - thread_id: Required[str] - - body: Required[Iterable[Body]] - - -class Body(TypedDict, total=False): - output: Optional[str] - - tool_call_id: Optional[str] diff --git a/tests/api_resources/knowledge_bases/test_sources.py b/tests/api_resources/knowledge_bases/test_sources.py index d75f1b1..bf69823 100644 --- a/tests/api_resources/knowledge_bases/test_sources.py +++ b/tests/api_resources/knowledge_bases/test_sources.py @@ -55,6 +55,7 @@ def test_method_create_with_all_params(self, client: Agility) -> None: "cron": "cron", "utc_offset": 0, }, + sync=True, ) assert_matches_type(Source, source, path=["response"]) @@ -194,6 +195,7 @@ def test_method_update_with_all_params(self, client: Agility) -> None: "cron": "cron", "utc_offset": 0, }, + sync=True, ) assert_matches_type(Source, source, path=["response"]) @@ -494,6 +496,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgility) - "cron": "cron", "utc_offset": 0, }, + sync=True, ) assert_matches_type(Source, source, path=["response"]) @@ -633,6 +636,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncAgility) - "cron": "cron", "utc_offset": 0, }, + sync=True, ) assert_matches_type(Source, source, path=["response"]) diff --git a/tests/api_resources/test_assistants.py b/tests/api_resources/test_assistants.py index f529674..bc9403b 100644 --- a/tests/api_resources/test_assistants.py +++ b/tests/api_resources/test_assistants.py @@ -10,6 +10,7 @@ from agility import Agility, AsyncAgility from tests.utils import assert_matches_type from agility.types import ( + Assistant, AssistantWithConfig, AssistantListResponse, ) @@ -27,7 +28,7 @@ def test_method_create(self, client: Agility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", name="name", ) - assert_matches_type(AssistantWithConfig, assistant, path=["response"]) + assert_matches_type(Assistant, assistant, path=["response"]) @parametrize def test_method_create_with_all_params(self, client: Agility) -> None: @@ -37,52 +38,10 @@ def test_method_create_with_all_params(self, client: Agility) -> None: name="name", instructions="instructions", model="gpt-4o", - tools=[ - { - "function": { - "description": "description", - "name": "name", - "parameters": { - "type": "type", - "additional_properties": True, - "properties": {"foo": "bar"}, - "required": ["string", "string", "string"], - }, - "strict": True, - }, - "type": "function", - }, - { - "function": { - "description": "description", - "name": "name", - "parameters": { - "type": "type", - "additional_properties": True, - "properties": {"foo": "bar"}, - "required": ["string", "string", "string"], - }, - "strict": True, - }, - "type": "function", - }, - { - "function": { - "description": "description", - "name": "name", - "parameters": { - "type": "type", - "additional_properties": True, - "properties": {"foo": "bar"}, - "required": ["string", "string", "string"], - }, - "strict": True, - }, - "type": "function", - }, - ], + suggested_questions=["string", "string", "string"], + url_slug="url_slug", ) - assert_matches_type(AssistantWithConfig, assistant, path=["response"]) + assert_matches_type(Assistant, assistant, path=["response"]) @parametrize def test_raw_response_create(self, client: Agility) -> None: @@ -95,7 +54,7 @@ def test_raw_response_create(self, client: Agility) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" assistant = response.parse() - assert_matches_type(AssistantWithConfig, assistant, path=["response"]) + assert_matches_type(Assistant, assistant, path=["response"]) @parametrize def test_streaming_response_create(self, client: Agility) -> None: @@ -108,7 +67,7 @@ def test_streaming_response_create(self, client: Agility) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" assistant = response.parse() - assert_matches_type(AssistantWithConfig, assistant, path=["response"]) + assert_matches_type(Assistant, assistant, path=["response"]) assert cast(Any, response.is_closed) is True @@ -171,50 +130,8 @@ def test_method_update_with_all_params(self, client: Agility) -> None: name="name", instructions="instructions", model="gpt-4o", - tools=[ - { - "function": { - "description": "description", - "name": "name", - "parameters": { - "type": "type", - "additional_properties": True, - "properties": {"foo": "bar"}, - "required": ["string", "string", "string"], - }, - "strict": True, - }, - "type": "function", - }, - { - "function": { - "description": "description", - "name": "name", - "parameters": { - "type": "type", - "additional_properties": True, - "properties": {"foo": "bar"}, - "required": ["string", "string", "string"], - }, - "strict": True, - }, - "type": "function", - }, - { - "function": { - "description": "description", - "name": "name", - "parameters": { - "type": "type", - "additional_properties": True, - "properties": {"foo": "bar"}, - "required": ["string", "string", "string"], - }, - "strict": True, - }, - "type": "function", - }, - ], + suggested_questions=["string", "string", "string"], + url_slug="url_slug", ) assert_matches_type(AssistantWithConfig, assistant, path=["response"]) @@ -343,7 +260,7 @@ async def test_method_create(self, async_client: AsyncAgility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", name="name", ) - assert_matches_type(AssistantWithConfig, assistant, path=["response"]) + assert_matches_type(Assistant, assistant, path=["response"]) @parametrize async def test_method_create_with_all_params(self, async_client: AsyncAgility) -> None: @@ -353,52 +270,10 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgility) - name="name", instructions="instructions", model="gpt-4o", - tools=[ - { - "function": { - "description": "description", - "name": "name", - "parameters": { - "type": "type", - "additional_properties": True, - "properties": {"foo": "bar"}, - "required": ["string", "string", "string"], - }, - "strict": True, - }, - "type": "function", - }, - { - "function": { - "description": "description", - "name": "name", - "parameters": { - "type": "type", - "additional_properties": True, - "properties": {"foo": "bar"}, - "required": ["string", "string", "string"], - }, - "strict": True, - }, - "type": "function", - }, - { - "function": { - "description": "description", - "name": "name", - "parameters": { - "type": "type", - "additional_properties": True, - "properties": {"foo": "bar"}, - "required": ["string", "string", "string"], - }, - "strict": True, - }, - "type": "function", - }, - ], + suggested_questions=["string", "string", "string"], + url_slug="url_slug", ) - assert_matches_type(AssistantWithConfig, assistant, path=["response"]) + assert_matches_type(Assistant, assistant, path=["response"]) @parametrize async def test_raw_response_create(self, async_client: AsyncAgility) -> None: @@ -411,7 +286,7 @@ async def test_raw_response_create(self, async_client: AsyncAgility) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" assistant = await response.parse() - assert_matches_type(AssistantWithConfig, assistant, path=["response"]) + assert_matches_type(Assistant, assistant, path=["response"]) @parametrize async def test_streaming_response_create(self, async_client: AsyncAgility) -> None: @@ -424,7 +299,7 @@ async def test_streaming_response_create(self, async_client: AsyncAgility) -> No assert response.http_request.headers.get("X-Stainless-Lang") == "python" assistant = await response.parse() - assert_matches_type(AssistantWithConfig, assistant, path=["response"]) + assert_matches_type(Assistant, assistant, path=["response"]) assert cast(Any, response.is_closed) is True @@ -487,50 +362,8 @@ async def test_method_update_with_all_params(self, async_client: AsyncAgility) - name="name", instructions="instructions", model="gpt-4o", - tools=[ - { - "function": { - "description": "description", - "name": "name", - "parameters": { - "type": "type", - "additional_properties": True, - "properties": {"foo": "bar"}, - "required": ["string", "string", "string"], - }, - "strict": True, - }, - "type": "function", - }, - { - "function": { - "description": "description", - "name": "name", - "parameters": { - "type": "type", - "additional_properties": True, - "properties": {"foo": "bar"}, - "required": ["string", "string", "string"], - }, - "strict": True, - }, - "type": "function", - }, - { - "function": { - "description": "description", - "name": "name", - "parameters": { - "type": "type", - "additional_properties": True, - "properties": {"foo": "bar"}, - "required": ["string", "string", "string"], - }, - "strict": True, - }, - "type": "function", - }, - ], + suggested_questions=["string", "string", "string"], + url_slug="url_slug", ) assert_matches_type(AssistantWithConfig, assistant, path=["response"]) diff --git a/tests/api_resources/threads/test_runs.py b/tests/api_resources/threads/test_runs.py index 1fb019d..d286533 100644 --- a/tests/api_resources/threads/test_runs.py +++ b/tests/api_resources/threads/test_runs.py @@ -54,50 +54,6 @@ def test_method_create_with_all_params(self, client: Agility) -> None: instructions="instructions", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", model="gpt-4o", - tools=[ - { - "function": { - "description": "description", - "name": "name", - "parameters": { - "type": "type", - "additional_properties": True, - "properties": {"foo": "bar"}, - "required": ["string", "string", "string"], - }, - "strict": True, - }, - "type": "function", - }, - { - "function": { - "description": "description", - "name": "name", - "parameters": { - "type": "type", - "additional_properties": True, - "properties": {"foo": "bar"}, - "required": ["string", "string", "string"], - }, - "strict": True, - }, - "type": "function", - }, - { - "function": { - "description": "description", - "name": "name", - "parameters": { - "type": "type", - "additional_properties": True, - "properties": {"foo": "bar"}, - "required": ["string", "string", "string"], - }, - "strict": True, - }, - "type": "function", - }, - ], ) assert_matches_type(Run, run, path=["response"]) @@ -268,50 +224,6 @@ def test_method_stream_with_all_params(self, client: Agility) -> None: instructions="instructions", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", model="gpt-4o", - tools=[ - { - "function": { - "description": "description", - "name": "name", - "parameters": { - "type": "type", - "additional_properties": True, - "properties": {"foo": "bar"}, - "required": ["string", "string", "string"], - }, - "strict": True, - }, - "type": "function", - }, - { - "function": { - "description": "description", - "name": "name", - "parameters": { - "type": "type", - "additional_properties": True, - "properties": {"foo": "bar"}, - "required": ["string", "string", "string"], - }, - "strict": True, - }, - "type": "function", - }, - { - "function": { - "description": "description", - "name": "name", - "parameters": { - "type": "type", - "additional_properties": True, - "properties": {"foo": "bar"}, - "required": ["string", "string", "string"], - }, - "strict": True, - }, - "type": "function", - }, - ], ) assert_matches_type(object, run, path=["response"]) @@ -349,59 +261,6 @@ def test_path_params_stream(self, client: Agility) -> None: assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) - @parametrize - def test_method_submit_tool_outputs(self, client: Agility) -> None: - run = client.threads.runs.submit_tool_outputs( - run_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - body=[{}, {}, {}], - ) - assert_matches_type(Run, run, path=["response"]) - - @parametrize - def test_raw_response_submit_tool_outputs(self, client: Agility) -> None: - response = client.threads.runs.with_raw_response.submit_tool_outputs( - run_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - body=[{}, {}, {}], - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - run = response.parse() - assert_matches_type(Run, run, path=["response"]) - - @parametrize - def test_streaming_response_submit_tool_outputs(self, client: Agility) -> None: - with client.threads.runs.with_streaming_response.submit_tool_outputs( - run_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - body=[{}, {}, {}], - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - run = response.parse() - assert_matches_type(Run, run, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @parametrize - def test_path_params_submit_tool_outputs(self, client: Agility) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `thread_id` but received ''"): - client.threads.runs.with_raw_response.submit_tool_outputs( - run_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - thread_id="", - body=[{}, {}, {}], - ) - - with pytest.raises(ValueError, match=r"Expected a non-empty value for `run_id` but received ''"): - client.threads.runs.with_raw_response.submit_tool_outputs( - run_id="", - thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - body=[{}, {}, {}], - ) - class TestAsyncRuns: parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) @@ -443,50 +302,6 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgility) - instructions="instructions", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", model="gpt-4o", - tools=[ - { - "function": { - "description": "description", - "name": "name", - "parameters": { - "type": "type", - "additional_properties": True, - "properties": {"foo": "bar"}, - "required": ["string", "string", "string"], - }, - "strict": True, - }, - "type": "function", - }, - { - "function": { - "description": "description", - "name": "name", - "parameters": { - "type": "type", - "additional_properties": True, - "properties": {"foo": "bar"}, - "required": ["string", "string", "string"], - }, - "strict": True, - }, - "type": "function", - }, - { - "function": { - "description": "description", - "name": "name", - "parameters": { - "type": "type", - "additional_properties": True, - "properties": {"foo": "bar"}, - "required": ["string", "string", "string"], - }, - "strict": True, - }, - "type": "function", - }, - ], ) assert_matches_type(Run, run, path=["response"]) @@ -657,50 +472,6 @@ async def test_method_stream_with_all_params(self, async_client: AsyncAgility) - instructions="instructions", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", model="gpt-4o", - tools=[ - { - "function": { - "description": "description", - "name": "name", - "parameters": { - "type": "type", - "additional_properties": True, - "properties": {"foo": "bar"}, - "required": ["string", "string", "string"], - }, - "strict": True, - }, - "type": "function", - }, - { - "function": { - "description": "description", - "name": "name", - "parameters": { - "type": "type", - "additional_properties": True, - "properties": {"foo": "bar"}, - "required": ["string", "string", "string"], - }, - "strict": True, - }, - "type": "function", - }, - { - "function": { - "description": "description", - "name": "name", - "parameters": { - "type": "type", - "additional_properties": True, - "properties": {"foo": "bar"}, - "required": ["string", "string", "string"], - }, - "strict": True, - }, - "type": "function", - }, - ], ) assert_matches_type(object, run, path=["response"]) @@ -737,56 +508,3 @@ async def test_path_params_stream(self, async_client: AsyncAgility) -> None: thread_id="", assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) - - @parametrize - async def test_method_submit_tool_outputs(self, async_client: AsyncAgility) -> None: - run = await async_client.threads.runs.submit_tool_outputs( - run_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - body=[{}, {}, {}], - ) - assert_matches_type(Run, run, path=["response"]) - - @parametrize - async def test_raw_response_submit_tool_outputs(self, async_client: AsyncAgility) -> None: - response = await async_client.threads.runs.with_raw_response.submit_tool_outputs( - run_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - body=[{}, {}, {}], - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - run = await response.parse() - assert_matches_type(Run, run, path=["response"]) - - @parametrize - async def test_streaming_response_submit_tool_outputs(self, async_client: AsyncAgility) -> None: - async with async_client.threads.runs.with_streaming_response.submit_tool_outputs( - run_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - body=[{}, {}, {}], - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - run = await response.parse() - assert_matches_type(Run, run, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @parametrize - async def test_path_params_submit_tool_outputs(self, async_client: AsyncAgility) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `thread_id` but received ''"): - await async_client.threads.runs.with_raw_response.submit_tool_outputs( - run_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - thread_id="", - body=[{}, {}, {}], - ) - - with pytest.raises(ValueError, match=r"Expected a non-empty value for `run_id` but received ''"): - await async_client.threads.runs.with_raw_response.submit_tool_outputs( - run_id="", - thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - body=[{}, {}, {}], - ) diff --git a/tests/conftest.py b/tests/conftest.py index 8421e24..8b10331 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 agility import Agility, AsyncAgility @@ -17,11 +17,13 @@ logging.getLogger("agility").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") diff --git a/tests/test_client.py b/tests/test_client.py index c62b67c..21c369f 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 @@ -735,7 +736,14 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> Non @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @mock.patch("agility._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - def test_retries_taken(self, client: Agility, failures_before_success: int, respx_mock: MockRouter) -> None: + @pytest.mark.parametrize("failure_mode", ["status", "exception"]) + def test_retries_taken( + self, + client: Agility, + failures_before_success: int, + failure_mode: Literal["status", "exception"], + respx_mock: MockRouter, + ) -> None: client = client.with_options(max_retries=4) nb_retries = 0 @@ -744,6 +752,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) @@ -1515,8 +1525,13 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) @mock.patch("agility._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: AsyncAgility, failures_before_success: int, respx_mock: MockRouter + self, + async_client: AsyncAgility, + failures_before_success: int, + failure_mode: Literal["status", "exception"], + respx_mock: MockRouter, ) -> None: client = async_client.with_options(max_retries=4) @@ -1526,6 +1541,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) From a000cdbb92ad6f0f1b31dc7e863905c3de55614a Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Fri, 1 Nov 2024 17:48:33 +0000 Subject: [PATCH 09/77] feat(api): manual updates --- .stats.yml | 2 +- README.md | 77 +++++++- api.md | 46 ++--- src/agility/__init__.py | 14 +- src/agility/_client.py | 182 +++++++++++++++--- src/agility/pagination.py | 78 ++++++++ src/agility/resources/__init__.py | 14 -- .../resources/assistants/access_keys.py | 106 ++-------- .../resources/assistants/assistants.py | 22 ++- src/agility/resources/health.py | 135 ------------- .../knowledge_bases/knowledge_bases.py | 22 ++- .../knowledge_bases/sources/documents.py | 27 ++- .../knowledge_bases/sources/sources.py | 22 ++- src/agility/resources/threads/messages.py | 22 ++- src/agility/resources/threads/threads.py | 27 ++- src/agility/types/__init__.py | 4 - src/agility/types/assistant_list_response.py | 10 - src/agility/types/assistants/__init__.py | 1 - .../assistants/access_key_list_response.py | 10 - src/agility/types/health_check_response.py | 10 - .../types/knowledge_base_list_response.py | 10 - src/agility/types/knowledge_bases/__init__.py | 1 - .../knowledge_bases/source_list_response.py | 10 - .../types/knowledge_bases/sources/__init__.py | 1 - .../sources/document_list_response.py | 10 - src/agility/types/thread_list_response.py | 10 - src/agility/types/threads/__init__.py | 1 - .../types/threads/message_list_response.py | 10 - .../assistants/test_access_keys.py | 115 +---------- .../knowledge_bases/sources/test_documents.py | 19 +- .../knowledge_bases/test_sources.py | 18 +- tests/api_resources/test_assistants.py | 18 +- tests/api_resources/test_health.py | 72 ------- tests/api_resources/test_knowledge_bases.py | 18 +- tests/api_resources/test_threads.py | 19 +- tests/api_resources/threads/test_messages.py | 19 +- tests/test_client.py | 18 ++ 37 files changed, 509 insertions(+), 691 deletions(-) create mode 100644 src/agility/pagination.py delete mode 100644 src/agility/resources/health.py delete mode 100644 src/agility/types/assistant_list_response.py delete mode 100644 src/agility/types/assistants/access_key_list_response.py delete mode 100644 src/agility/types/health_check_response.py delete mode 100644 src/agility/types/knowledge_base_list_response.py delete mode 100644 src/agility/types/knowledge_bases/source_list_response.py delete mode 100644 src/agility/types/knowledge_bases/sources/document_list_response.py delete mode 100644 src/agility/types/thread_list_response.py delete mode 100644 src/agility/types/threads/message_list_response.py delete mode 100644 tests/api_resources/test_health.py diff --git a/.stats.yml b/.stats.yml index d06e34a..479db52 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ -configured_endpoints: 38 +configured_endpoints: 36 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-36e7c2cdfead9cc1628d9e75a9be97a5580130287e8610c5bceac14770743512.yml diff --git a/README.md b/README.md index d74c442..3b8a978 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,10 @@ The full API of this library can be found in [api.md](api.md). ```python from agility import Agility -client = Agility() +client = Agility( + # or 'production' | 'dev' | 'local'; defaults to "production". + environment="staging", +) assistant = client.assistants.create( description="description", @@ -39,10 +42,10 @@ assistant = client.assistants.create( print(assistant.id) ``` -While you can provide an `api_key` keyword argument, +While you can provide a `bearer_token` keyword argument, we recommend using [python-dotenv](https://pypi.org/project/python-dotenv/) -to add `AUTHENTICATED_API_KEY="My API Key"` to your `.env` file -so that your API Key is not stored in source control. +to add `BEARER_TOKEN="My Bearer Token"` to your `.env` file +so that your Bearer Token is not stored in source control. ## Async usage @@ -52,7 +55,10 @@ Simply import `AsyncAgility` instead of `Agility` and use `await` with each API import asyncio from agility import AsyncAgility -client = AsyncAgility() +client = AsyncAgility( + # or 'production' | 'dev' | 'local'; defaults to "production". + environment="staging", +) async def main() -> None: @@ -78,6 +84,67 @@ 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 Agility 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 agility import Agility + +client = Agility() + +all_assistants = [] +# Automatically fetches more pages as needed. +for assistant in client.assistants.list(): + # Do something with assistant here + all_assistants.append(assistant) +print(all_assistants) +``` + +Or, asynchronously: + +```python +import asyncio +from agility import AsyncAgility + +client = AsyncAgility() + + +async def main() -> None: + all_assistants = [] + # Iterate through items across all pages, issuing requests as needed. + async for assistant in client.assistants.list(): + all_assistants.append(assistant) + print(all_assistants) + + +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.assistants.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.items)}") + +# Remove `await` for non-async usage. +``` + +Or just work directly with the returned data: + +```python +first_page = await client.assistants.list() +for assistant in first_page.items: + print(assistant.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 `agility.APIConnectionError` is raised. diff --git a/api.md b/api.md index e2533b5..7cca472 100644 --- a/api.md +++ b/api.md @@ -3,7 +3,7 @@ Types: ```python -from agility.types import Assistant, AssistantWithConfig, AssistantListResponse +from agility.types import Assistant, AssistantWithConfig ``` Methods: @@ -11,7 +11,7 @@ Methods: - client.assistants.create(\*\*params) -> Assistant - client.assistants.retrieve(assistant_id) -> AssistantWithConfig - client.assistants.update(assistant_id, \*\*params) -> AssistantWithConfig -- client.assistants.list(\*\*params) -> AssistantListResponse +- client.assistants.list(\*\*params) -> SyncMyOffsetPage[AssistantWithConfig] - client.assistants.delete(assistant_id) -> None ## AccessKeys @@ -19,21 +19,20 @@ Methods: Types: ```python -from agility.types.assistants import AccessKey, AccessKeyListResponse +from agility.types.assistants import AccessKey ``` Methods: - client.assistants.access_keys.create(assistant_id, \*\*params) -> AccessKey -- client.assistants.access_keys.retrieve(access_key_id, \*, assistant_id) -> AccessKey -- client.assistants.access_keys.list(assistant_id, \*\*params) -> AccessKeyListResponse +- client.assistants.access_keys.list(assistant_id, \*\*params) -> SyncMyOffsetPage[AccessKey] # KnowledgeBases Types: ```python -from agility.types import KnowledgeBaseWithConfig, KnowledgeBaseListResponse +from agility.types import KnowledgeBaseWithConfig ``` Methods: @@ -41,7 +40,7 @@ Methods: - client.knowledge_bases.create(\*\*params) -> KnowledgeBaseWithConfig - client.knowledge_bases.retrieve(knowledge_base_id) -> KnowledgeBaseWithConfig - client.knowledge_bases.update(knowledge_base_id, \*\*params) -> KnowledgeBaseWithConfig -- client.knowledge_bases.list(\*\*params) -> KnowledgeBaseListResponse +- client.knowledge_bases.list(\*\*params) -> SyncMyOffsetPage[KnowledgeBaseWithConfig] - client.knowledge_bases.delete(knowledge_base_id) -> None ## Sources @@ -49,12 +48,7 @@ Methods: Types: ```python -from agility.types.knowledge_bases import ( - Source, - SourceStatusResponse, - SourceListResponse, - SourceSyncResponse, -) +from agility.types.knowledge_bases import Source, SourceStatusResponse, SourceSyncResponse ``` Methods: @@ -62,7 +56,7 @@ Methods: - client.knowledge_bases.sources.create(knowledge_base_id, \*\*params) -> Source - client.knowledge_bases.sources.retrieve(source_id, \*, knowledge_base_id) -> Source - client.knowledge_bases.sources.update(source_id, \*, knowledge_base_id, \*\*params) -> Source -- client.knowledge_bases.sources.list(knowledge_base_id, \*\*params) -> SourceListResponse +- client.knowledge_bases.sources.list(knowledge_base_id, \*\*params) -> SyncMyOffsetPage[Source] - client.knowledge_bases.sources.delete(source_id, \*, knowledge_base_id) -> None - client.knowledge_bases.sources.status(source_id, \*, knowledge_base_id) -> SourceStatusResponse - client.knowledge_bases.sources.sync(source_id, \*, knowledge_base_id) -> object @@ -72,25 +66,13 @@ Methods: Types: ```python -from agility.types.knowledge_bases.sources import Document, DocumentListResponse +from agility.types.knowledge_bases.sources import Document ``` Methods: - client.knowledge_bases.sources.documents.retrieve(document_id, \*, knowledge_base_id, source_id) -> Document -- client.knowledge_bases.sources.documents.list(source_id, \*, knowledge_base_id, \*\*params) -> DocumentListResponse - -# Health - -Types: - -```python -from agility.types import HealthCheckResponse -``` - -Methods: - -- client.health.check() -> HealthCheckResponse +- client.knowledge_bases.sources.documents.list(source_id, \*, knowledge_base_id, \*\*params) -> SyncMyOffsetPage[Document] # Users @@ -122,14 +104,14 @@ Methods: Types: ```python -from agility.types import Thread, ThreadListResponse +from agility.types import Thread ``` Methods: - client.threads.create() -> Thread - client.threads.retrieve(thread_id) -> Thread -- client.threads.list(\*\*params) -> ThreadListResponse +- client.threads.list(\*\*params) -> SyncMyOffsetPage[Thread] - client.threads.delete(thread_id) -> None ## Messages @@ -137,14 +119,14 @@ Methods: Types: ```python -from agility.types.threads import Message, MessageListResponse +from agility.types.threads import Message ``` Methods: - client.threads.messages.create(thread_id, \*\*params) -> Message - client.threads.messages.retrieve(message_id, \*, thread_id) -> Message -- client.threads.messages.list(thread_id, \*\*params) -> MessageListResponse +- client.threads.messages.list(thread_id, \*\*params) -> SyncMyOffsetPage[Message] - client.threads.messages.delete(message_id, \*, thread_id) -> None ## Runs diff --git a/src/agility/__init__.py b/src/agility/__init__.py index 5569675..1e77292 100644 --- a/src/agility/__init__.py +++ b/src/agility/__init__.py @@ -3,7 +3,18 @@ from . import types from ._types import NOT_GIVEN, NoneType, NotGiven, Transport, ProxiesTypes from ._utils import file_from_path -from ._client import Client, Stream, Agility, Timeout, Transport, AsyncClient, AsyncStream, AsyncAgility, RequestOptions +from ._client import ( + ENVIRONMENTS, + Client, + Stream, + Agility, + Timeout, + Transport, + AsyncClient, + AsyncStream, + AsyncAgility, + RequestOptions, +) from ._models import BaseModel from ._version import __title__, __version__ from ._response import APIResponse as APIResponse, AsyncAPIResponse as AsyncAPIResponse @@ -58,6 +69,7 @@ "AsyncStream", "Agility", "AsyncAgility", + "ENVIRONMENTS", "file_from_path", "BaseModel", "DEFAULT_TIMEOUT", diff --git a/src/agility/_client.py b/src/agility/_client.py index 2119303..4eb4597 100644 --- a/src/agility/_client.py +++ b/src/agility/_client.py @@ -3,8 +3,8 @@ from __future__ import annotations import os -from typing import Any, Union, Mapping -from typing_extensions import Self, override +from typing import Any, Dict, Union, Mapping, cast +from typing_extensions import Self, Literal, override import httpx @@ -33,6 +33,7 @@ ) __all__ = [ + "ENVIRONMENTS", "Timeout", "Transport", "ProxiesTypes", @@ -44,24 +45,37 @@ "AsyncClient", ] +ENVIRONMENTS: Dict[str, str] = { + "production": "https://api-agility.cleanlab.ai", + "staging": "https://api-agility.staging-bc26qf4m.cleanlab.ai", + "dev": "https://api-agility.dev-bc26qf4m.cleanlab.ai", + "local": "http://localhost:8080", +} + class Agility(SyncAPIClient): assistants: resources.AssistantsResource knowledge_bases: resources.KnowledgeBasesResource - health: resources.HealthResource users: resources.UsersResource threads: resources.ThreadsResource with_raw_response: AgilityWithRawResponse with_streaming_response: AgilityWithStreamedResponse # client options + bearer_token: str | None api_key: str + access_key: str | None + + _environment: Literal["production", "staging", "dev", "local"] | NotGiven def __init__( self, *, + bearer_token: str | None = None, api_key: str | None = None, - base_url: str | httpx.URL | None = None, + access_key: str | None = None, + environment: Literal["production", "staging", "dev", "local"] | NotGiven = NOT_GIVEN, + base_url: str | httpx.URL | None | NotGiven = NOT_GIVEN, timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN, max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, @@ -82,8 +96,15 @@ def __init__( ) -> None: """Construct a new synchronous agility client instance. - This automatically infers the `api_key` argument from the `AUTHENTICATED_API_KEY` environment variable if it is not provided. + This automatically infers the following arguments from their corresponding environment variables if they are not provided: + - `bearer_token` from `BEARER_TOKEN` + - `api_key` from `AUTHENTICATED_API_KEY` + - `access_key` from `PUBLIC_ACCESS_KEY` """ + if bearer_token is None: + bearer_token = os.environ.get("BEARER_TOKEN") + self.bearer_token = bearer_token + if api_key is None: api_key = os.environ.get("AUTHENTICATED_API_KEY") if api_key is None: @@ -92,10 +113,35 @@ def __init__( ) self.api_key = api_key - if base_url is None: - base_url = os.environ.get("AGILITY_BASE_URL") - if base_url is None: - base_url = f"http://localhost:8080" + if access_key is None: + access_key = os.environ.get("PUBLIC_ACCESS_KEY") + self.access_key = access_key + + self._environment = environment + + base_url_env = os.environ.get("AGILITY_BASE_URL") + if is_given(base_url) and base_url is not None: + # cast required because mypy doesn't understand the type narrowing + base_url = cast("str | httpx.URL", base_url) # pyright: ignore[reportUnnecessaryCast] + elif is_given(environment): + if base_url_env and base_url is not None: + raise ValueError( + "Ambiguous URL; The `AGILITY_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None", + ) + + try: + base_url = ENVIRONMENTS[environment] + except KeyError as exc: + raise ValueError(f"Unknown environment: {environment}") from exc + elif base_url_env is not None: + base_url = base_url_env + else: + self._environment = environment = "production" + + try: + base_url = ENVIRONMENTS[environment] + except KeyError as exc: + raise ValueError(f"Unknown environment: {environment}") from exc super().__init__( version=__version__, @@ -110,7 +156,6 @@ def __init__( self.assistants = resources.AssistantsResource(self) self.knowledge_bases = resources.KnowledgeBasesResource(self) - self.health = resources.HealthResource(self) self.users = resources.UsersResource(self) self.threads = resources.ThreadsResource(self) self.with_raw_response = AgilityWithRawResponse(self) @@ -124,9 +169,33 @@ def qs(self) -> Querystring: @property @override def auth_headers(self) -> dict[str, str]: + if self._http_bearer: + return self._http_bearer + if self._authenticated_api_key: + return self._authenticated_api_key + if self._public_access_key: + return self._public_access_key + return {} + + @property + def _http_bearer(self) -> dict[str, str]: + bearer_token = self.bearer_token + if bearer_token is None: + return {} + return {"Authorization": f"Bearer {bearer_token}"} + + @property + def _authenticated_api_key(self) -> dict[str, str]: api_key = self.api_key return {"X-API-Key": api_key} + @property + def _public_access_key(self) -> dict[str, str]: + access_key = self.access_key + if access_key is None: + return {} + return {"X-Access-Key": access_key} + @property @override def default_headers(self) -> dict[str, str | Omit]: @@ -139,7 +208,10 @@ def default_headers(self) -> dict[str, str | Omit]: def copy( self, *, + bearer_token: str | None = None, api_key: str | None = None, + access_key: str | None = None, + environment: Literal["production", "staging", "dev", "local"] | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = NOT_GIVEN, http_client: httpx.Client | None = None, @@ -173,8 +245,11 @@ def copy( http_client = http_client or self._client return self.__class__( + bearer_token=bearer_token or self.bearer_token, api_key=api_key or self.api_key, + access_key=access_key or self.access_key, base_url=base_url or self.base_url, + environment=environment or self._environment, timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, http_client=http_client, max_retries=max_retries if is_given(max_retries) else self.max_retries, @@ -224,20 +299,26 @@ def _make_status_error( class AsyncAgility(AsyncAPIClient): assistants: resources.AsyncAssistantsResource knowledge_bases: resources.AsyncKnowledgeBasesResource - health: resources.AsyncHealthResource users: resources.AsyncUsersResource threads: resources.AsyncThreadsResource with_raw_response: AsyncAgilityWithRawResponse with_streaming_response: AsyncAgilityWithStreamedResponse # client options + bearer_token: str | None api_key: str + access_key: str | None + + _environment: Literal["production", "staging", "dev", "local"] | NotGiven def __init__( self, *, + bearer_token: str | None = None, api_key: str | None = None, - base_url: str | httpx.URL | None = None, + access_key: str | None = None, + environment: Literal["production", "staging", "dev", "local"] | NotGiven = NOT_GIVEN, + base_url: str | httpx.URL | None | NotGiven = NOT_GIVEN, timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN, max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, @@ -258,8 +339,15 @@ def __init__( ) -> None: """Construct a new async agility client instance. - This automatically infers the `api_key` argument from the `AUTHENTICATED_API_KEY` environment variable if it is not provided. + This automatically infers the following arguments from their corresponding environment variables if they are not provided: + - `bearer_token` from `BEARER_TOKEN` + - `api_key` from `AUTHENTICATED_API_KEY` + - `access_key` from `PUBLIC_ACCESS_KEY` """ + if bearer_token is None: + bearer_token = os.environ.get("BEARER_TOKEN") + self.bearer_token = bearer_token + if api_key is None: api_key = os.environ.get("AUTHENTICATED_API_KEY") if api_key is None: @@ -268,10 +356,35 @@ def __init__( ) self.api_key = api_key - if base_url is None: - base_url = os.environ.get("AGILITY_BASE_URL") - if base_url is None: - base_url = f"http://localhost:8080" + if access_key is None: + access_key = os.environ.get("PUBLIC_ACCESS_KEY") + self.access_key = access_key + + self._environment = environment + + base_url_env = os.environ.get("AGILITY_BASE_URL") + if is_given(base_url) and base_url is not None: + # cast required because mypy doesn't understand the type narrowing + base_url = cast("str | httpx.URL", base_url) # pyright: ignore[reportUnnecessaryCast] + elif is_given(environment): + if base_url_env and base_url is not None: + raise ValueError( + "Ambiguous URL; The `AGILITY_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None", + ) + + try: + base_url = ENVIRONMENTS[environment] + except KeyError as exc: + raise ValueError(f"Unknown environment: {environment}") from exc + elif base_url_env is not None: + base_url = base_url_env + else: + self._environment = environment = "production" + + try: + base_url = ENVIRONMENTS[environment] + except KeyError as exc: + raise ValueError(f"Unknown environment: {environment}") from exc super().__init__( version=__version__, @@ -286,7 +399,6 @@ def __init__( self.assistants = resources.AsyncAssistantsResource(self) self.knowledge_bases = resources.AsyncKnowledgeBasesResource(self) - self.health = resources.AsyncHealthResource(self) self.users = resources.AsyncUsersResource(self) self.threads = resources.AsyncThreadsResource(self) self.with_raw_response = AsyncAgilityWithRawResponse(self) @@ -300,9 +412,33 @@ def qs(self) -> Querystring: @property @override def auth_headers(self) -> dict[str, str]: + if self._http_bearer: + return self._http_bearer + if self._authenticated_api_key: + return self._authenticated_api_key + if self._public_access_key: + return self._public_access_key + return {} + + @property + def _http_bearer(self) -> dict[str, str]: + bearer_token = self.bearer_token + if bearer_token is None: + return {} + return {"Authorization": f"Bearer {bearer_token}"} + + @property + def _authenticated_api_key(self) -> dict[str, str]: api_key = self.api_key return {"X-API-Key": api_key} + @property + def _public_access_key(self) -> dict[str, str]: + access_key = self.access_key + if access_key is None: + return {} + return {"X-Access-Key": access_key} + @property @override def default_headers(self) -> dict[str, str | Omit]: @@ -315,7 +451,10 @@ def default_headers(self) -> dict[str, str | Omit]: def copy( self, *, + bearer_token: str | None = None, api_key: str | None = None, + access_key: str | None = None, + environment: Literal["production", "staging", "dev", "local"] | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = NOT_GIVEN, http_client: httpx.AsyncClient | None = None, @@ -349,8 +488,11 @@ def copy( http_client = http_client or self._client return self.__class__( + bearer_token=bearer_token or self.bearer_token, api_key=api_key or self.api_key, + access_key=access_key or self.access_key, base_url=base_url or self.base_url, + environment=environment or self._environment, timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, http_client=http_client, max_retries=max_retries if is_given(max_retries) else self.max_retries, @@ -401,7 +543,6 @@ class AgilityWithRawResponse: def __init__(self, client: Agility) -> None: self.assistants = resources.AssistantsResourceWithRawResponse(client.assistants) self.knowledge_bases = resources.KnowledgeBasesResourceWithRawResponse(client.knowledge_bases) - self.health = resources.HealthResourceWithRawResponse(client.health) self.users = resources.UsersResourceWithRawResponse(client.users) self.threads = resources.ThreadsResourceWithRawResponse(client.threads) @@ -410,7 +551,6 @@ class AsyncAgilityWithRawResponse: def __init__(self, client: AsyncAgility) -> None: self.assistants = resources.AsyncAssistantsResourceWithRawResponse(client.assistants) self.knowledge_bases = resources.AsyncKnowledgeBasesResourceWithRawResponse(client.knowledge_bases) - self.health = resources.AsyncHealthResourceWithRawResponse(client.health) self.users = resources.AsyncUsersResourceWithRawResponse(client.users) self.threads = resources.AsyncThreadsResourceWithRawResponse(client.threads) @@ -419,7 +559,6 @@ class AgilityWithStreamedResponse: def __init__(self, client: Agility) -> None: self.assistants = resources.AssistantsResourceWithStreamingResponse(client.assistants) self.knowledge_bases = resources.KnowledgeBasesResourceWithStreamingResponse(client.knowledge_bases) - self.health = resources.HealthResourceWithStreamingResponse(client.health) self.users = resources.UsersResourceWithStreamingResponse(client.users) self.threads = resources.ThreadsResourceWithStreamingResponse(client.threads) @@ -428,7 +567,6 @@ class AsyncAgilityWithStreamedResponse: def __init__(self, client: AsyncAgility) -> None: self.assistants = resources.AsyncAssistantsResourceWithStreamingResponse(client.assistants) self.knowledge_bases = resources.AsyncKnowledgeBasesResourceWithStreamingResponse(client.knowledge_bases) - self.health = resources.AsyncHealthResourceWithStreamingResponse(client.health) self.users = resources.AsyncUsersResourceWithStreamingResponse(client.users) self.threads = resources.AsyncThreadsResourceWithStreamingResponse(client.threads) diff --git a/src/agility/pagination.py b/src/agility/pagination.py new file mode 100644 index 0000000..963a431 --- /dev/null +++ b/src/agility/pagination.py @@ -0,0 +1,78 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Any, List, Type, Generic, Mapping, TypeVar, Optional, cast +from typing_extensions import override + +from httpx import Response + +from ._utils import is_mapping +from ._models import BaseModel +from ._base_client import BasePage, PageInfo, BaseSyncPage, BaseAsyncPage + +__all__ = ["SyncMyOffsetPage", "AsyncMyOffsetPage"] + +_BaseModelT = TypeVar("_BaseModelT", bound=BaseModel) + +_T = TypeVar("_T") + + +class SyncMyOffsetPage(BaseSyncPage[_T], BasePage[_T], Generic[_T]): + items: List[_T] + + @override + def _get_page_items(self) -> List[_T]: + items = self.items + if not items: + return [] + return items + + @override + def next_page_info(self) -> Optional[PageInfo]: + offset = self._options.params.get("offset") or 0 + if not isinstance(offset, int): + raise ValueError(f'Expected "offset" param to be an integer but got {offset}') + + length = len(self._get_page_items()) + current_count = offset + length + + return PageInfo(params={"offset": current_count}) + + @classmethod + def build(cls: Type[_BaseModelT], *, response: Response, data: object) -> _BaseModelT: # noqa: ARG003 + return cls.construct( + None, + **{ + **(cast(Mapping[str, Any], data) if is_mapping(data) else {"items": data}), + }, + ) + + +class AsyncMyOffsetPage(BaseAsyncPage[_T], BasePage[_T], Generic[_T]): + items: List[_T] + + @override + def _get_page_items(self) -> List[_T]: + items = self.items + if not items: + return [] + return items + + @override + def next_page_info(self) -> Optional[PageInfo]: + offset = self._options.params.get("offset") or 0 + if not isinstance(offset, int): + raise ValueError(f'Expected "offset" param to be an integer but got {offset}') + + length = len(self._get_page_items()) + current_count = offset + length + + return PageInfo(params={"offset": current_count}) + + @classmethod + def build(cls: Type[_BaseModelT], *, response: Response, data: object) -> _BaseModelT: # noqa: ARG003 + return cls.construct( + None, + **{ + **(cast(Mapping[str, Any], data) if is_mapping(data) else {"items": data}), + }, + ) diff --git a/src/agility/resources/__init__.py b/src/agility/resources/__init__.py index 02aa199..ae9ca9a 100644 --- a/src/agility/resources/__init__.py +++ b/src/agility/resources/__init__.py @@ -8,14 +8,6 @@ UsersResourceWithStreamingResponse, AsyncUsersResourceWithStreamingResponse, ) -from .health import ( - HealthResource, - AsyncHealthResource, - HealthResourceWithRawResponse, - AsyncHealthResourceWithRawResponse, - HealthResourceWithStreamingResponse, - AsyncHealthResourceWithStreamingResponse, -) from .threads import ( ThreadsResource, AsyncThreadsResource, @@ -54,12 +46,6 @@ "AsyncKnowledgeBasesResourceWithRawResponse", "KnowledgeBasesResourceWithStreamingResponse", "AsyncKnowledgeBasesResourceWithStreamingResponse", - "HealthResource", - "AsyncHealthResource", - "HealthResourceWithRawResponse", - "AsyncHealthResourceWithRawResponse", - "HealthResourceWithStreamingResponse", - "AsyncHealthResourceWithStreamingResponse", "UsersResource", "AsyncUsersResource", "UsersResourceWithRawResponse", diff --git a/src/agility/resources/assistants/access_keys.py b/src/agility/resources/assistants/access_keys.py index 0537699..80d11d9 100644 --- a/src/agility/resources/assistants/access_keys.py +++ b/src/agility/resources/assistants/access_keys.py @@ -20,10 +20,10 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from ..._base_client import make_request_options +from ...pagination import SyncMyOffsetPage, AsyncMyOffsetPage +from ..._base_client import AsyncPaginator, make_request_options from ...types.assistants import access_key_list_params, access_key_create_params from ...types.assistants.access_key import AccessKey -from ...types.assistants.access_key_list_response import AccessKeyListResponse __all__ = ["AccessKeysResource", "AsyncAccessKeysResource"] @@ -92,42 +92,6 @@ def create( cast_to=AccessKey, ) - def retrieve( - self, - access_key_id: str, - *, - assistant_id: str, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AccessKey: - """ - Get a access_key by ID. - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not assistant_id: - raise ValueError(f"Expected a non-empty value for `assistant_id` but received {assistant_id!r}") - if not access_key_id: - raise ValueError(f"Expected a non-empty value for `access_key_id` but received {access_key_id!r}") - return self._get( - f"/api/assistants/{assistant_id}/access_keys/{access_key_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AccessKey, - ) - def list( self, assistant_id: str, @@ -140,7 +104,7 @@ def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AccessKeyListResponse: + ) -> SyncMyOffsetPage[AccessKey]: """ List all access keys for an assistant. @@ -155,8 +119,9 @@ def list( """ if not assistant_id: raise ValueError(f"Expected a non-empty value for `assistant_id` but received {assistant_id!r}") - return self._get( + return self._get_api_list( f"/api/assistants/{assistant_id}/access_keys/", + page=SyncMyOffsetPage[AccessKey], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -170,7 +135,7 @@ def list( access_key_list_params.AccessKeyListParams, ), ), - cast_to=AccessKeyListResponse, + model=AccessKey, ) @@ -238,43 +203,7 @@ async def create( cast_to=AccessKey, ) - async def retrieve( - self, - access_key_id: str, - *, - assistant_id: str, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AccessKey: - """ - Get a access_key by ID. - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not assistant_id: - raise ValueError(f"Expected a non-empty value for `assistant_id` but received {assistant_id!r}") - if not access_key_id: - raise ValueError(f"Expected a non-empty value for `access_key_id` but received {access_key_id!r}") - return await self._get( - f"/api/assistants/{assistant_id}/access_keys/{access_key_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AccessKey, - ) - - async def list( + def list( self, assistant_id: str, *, @@ -286,7 +215,7 @@ async def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AccessKeyListResponse: + ) -> AsyncPaginator[AccessKey, AsyncMyOffsetPage[AccessKey]]: """ List all access keys for an assistant. @@ -301,14 +230,15 @@ async def list( """ if not assistant_id: raise ValueError(f"Expected a non-empty value for `assistant_id` but received {assistant_id!r}") - return await self._get( + return self._get_api_list( f"/api/assistants/{assistant_id}/access_keys/", + page=AsyncMyOffsetPage[AccessKey], 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, @@ -316,7 +246,7 @@ async def list( access_key_list_params.AccessKeyListParams, ), ), - cast_to=AccessKeyListResponse, + model=AccessKey, ) @@ -327,9 +257,6 @@ def __init__(self, access_keys: AccessKeysResource) -> None: self.create = to_raw_response_wrapper( access_keys.create, ) - self.retrieve = to_raw_response_wrapper( - access_keys.retrieve, - ) self.list = to_raw_response_wrapper( access_keys.list, ) @@ -342,9 +269,6 @@ def __init__(self, access_keys: AsyncAccessKeysResource) -> None: self.create = async_to_raw_response_wrapper( access_keys.create, ) - self.retrieve = async_to_raw_response_wrapper( - access_keys.retrieve, - ) self.list = async_to_raw_response_wrapper( access_keys.list, ) @@ -357,9 +281,6 @@ def __init__(self, access_keys: AccessKeysResource) -> None: self.create = to_streamed_response_wrapper( access_keys.create, ) - self.retrieve = to_streamed_response_wrapper( - access_keys.retrieve, - ) self.list = to_streamed_response_wrapper( access_keys.list, ) @@ -372,9 +293,6 @@ def __init__(self, access_keys: AsyncAccessKeysResource) -> None: self.create = async_to_streamed_response_wrapper( access_keys.create, ) - self.retrieve = async_to_streamed_response_wrapper( - access_keys.retrieve, - ) self.list = async_to_streamed_response_wrapper( access_keys.list, ) diff --git a/src/agility/resources/assistants/assistants.py b/src/agility/resources/assistants/assistants.py index 023e47c..5ba8de9 100644 --- a/src/agility/resources/assistants/assistants.py +++ b/src/agility/resources/assistants/assistants.py @@ -29,10 +29,10 @@ AccessKeysResourceWithStreamingResponse, AsyncAccessKeysResourceWithStreamingResponse, ) -from ..._base_client import make_request_options +from ...pagination import SyncMyOffsetPage, AsyncMyOffsetPage +from ..._base_client import AsyncPaginator, make_request_options from ...types.assistant import Assistant from ...types.assistant_with_config import AssistantWithConfig -from ...types.assistant_list_response import AssistantListResponse __all__ = ["AssistantsResource", "AsyncAssistantsResource"] @@ -224,7 +224,7 @@ def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AssistantListResponse: + ) -> SyncMyOffsetPage[AssistantWithConfig]: """ Get all assistants for the current user. @@ -237,8 +237,9 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ - return self._get( + return self._get_api_list( "/api/assistants/", + page=SyncMyOffsetPage[AssistantWithConfig], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -252,7 +253,7 @@ def list( assistant_list_params.AssistantListParams, ), ), - cast_to=AssistantListResponse, + model=AssistantWithConfig, ) def delete( @@ -466,7 +467,7 @@ async def update( cast_to=AssistantWithConfig, ) - async def list( + def list( self, *, limit: int | NotGiven = NOT_GIVEN, @@ -477,7 +478,7 @@ async def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AssistantListResponse: + ) -> AsyncPaginator[AssistantWithConfig, AsyncMyOffsetPage[AssistantWithConfig]]: """ Get all assistants for the current user. @@ -490,14 +491,15 @@ async def list( timeout: Override the client-level default timeout for this request, in seconds """ - return await self._get( + return self._get_api_list( "/api/assistants/", + page=AsyncMyOffsetPage[AssistantWithConfig], 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, @@ -505,7 +507,7 @@ async def list( assistant_list_params.AssistantListParams, ), ), - cast_to=AssistantListResponse, + model=AssistantWithConfig, ) async def delete( diff --git a/src/agility/resources/health.py b/src/agility/resources/health.py deleted file mode 100644 index 28b4065..0000000 --- a/src/agility/resources/health.py +++ /dev/null @@ -1,135 +0,0 @@ -# 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.health_check_response import HealthCheckResponse - -__all__ = ["HealthResource", "AsyncHealthResource"] - - -class HealthResource(SyncAPIResource): - @cached_property - def with_raw_response(self) -> HealthResourceWithRawResponse: - """ - 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers - """ - return HealthResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> HealthResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/stainless-sdks/agility-python#with_streaming_response - """ - return HealthResourceWithStreamingResponse(self) - - def check( - 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, - ) -> HealthCheckResponse: - """Check the health of the application.""" - return self._get( - "/api/health/", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=HealthCheckResponse, - ) - - -class AsyncHealthResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncHealthResourceWithRawResponse: - """ - 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers - """ - return AsyncHealthResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncHealthResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/stainless-sdks/agility-python#with_streaming_response - """ - return AsyncHealthResourceWithStreamingResponse(self) - - async def check( - 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, - ) -> HealthCheckResponse: - """Check the health of the application.""" - return await self._get( - "/api/health/", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=HealthCheckResponse, - ) - - -class HealthResourceWithRawResponse: - def __init__(self, health: HealthResource) -> None: - self._health = health - - self.check = to_raw_response_wrapper( - health.check, - ) - - -class AsyncHealthResourceWithRawResponse: - def __init__(self, health: AsyncHealthResource) -> None: - self._health = health - - self.check = async_to_raw_response_wrapper( - health.check, - ) - - -class HealthResourceWithStreamingResponse: - def __init__(self, health: HealthResource) -> None: - self._health = health - - self.check = to_streamed_response_wrapper( - health.check, - ) - - -class AsyncHealthResourceWithStreamingResponse: - def __init__(self, health: AsyncHealthResource) -> None: - self._health = health - - self.check = async_to_streamed_response_wrapper( - health.check, - ) diff --git a/src/agility/resources/knowledge_bases/knowledge_bases.py b/src/agility/resources/knowledge_bases/knowledge_bases.py index 15f51a2..c55b657 100644 --- a/src/agility/resources/knowledge_bases/knowledge_bases.py +++ b/src/agility/resources/knowledge_bases/knowledge_bases.py @@ -30,10 +30,10 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from ..._base_client import make_request_options +from ...pagination import SyncMyOffsetPage, AsyncMyOffsetPage +from ..._base_client import AsyncPaginator, make_request_options from .sources.sources import SourcesResource, AsyncSourcesResource from ...types.knowledge_base_with_config import KnowledgeBaseWithConfig -from ...types.knowledge_base_list_response import KnowledgeBaseListResponse __all__ = ["KnowledgeBasesResource", "AsyncKnowledgeBasesResource"] @@ -199,7 +199,7 @@ def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> KnowledgeBaseListResponse: + ) -> SyncMyOffsetPage[KnowledgeBaseWithConfig]: """ List all knowledge bases. @@ -212,8 +212,9 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ - return self._get( + return self._get_api_list( "/api/knowledge_bases/", + page=SyncMyOffsetPage[KnowledgeBaseWithConfig], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -227,7 +228,7 @@ def list( knowledge_base_list_params.KnowledgeBaseListParams, ), ), - cast_to=KnowledgeBaseListResponse, + model=KnowledgeBaseWithConfig, ) def delete( @@ -415,7 +416,7 @@ async def update( cast_to=KnowledgeBaseWithConfig, ) - async def list( + def list( self, *, limit: int | NotGiven = NOT_GIVEN, @@ -426,7 +427,7 @@ async def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> KnowledgeBaseListResponse: + ) -> AsyncPaginator[KnowledgeBaseWithConfig, AsyncMyOffsetPage[KnowledgeBaseWithConfig]]: """ List all knowledge bases. @@ -439,14 +440,15 @@ async def list( timeout: Override the client-level default timeout for this request, in seconds """ - return await self._get( + return self._get_api_list( "/api/knowledge_bases/", + page=AsyncMyOffsetPage[KnowledgeBaseWithConfig], 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, @@ -454,7 +456,7 @@ async def list( knowledge_base_list_params.KnowledgeBaseListParams, ), ), - cast_to=KnowledgeBaseListResponse, + model=KnowledgeBaseWithConfig, ) async def delete( diff --git a/src/agility/resources/knowledge_bases/sources/documents.py b/src/agility/resources/knowledge_bases/sources/documents.py index 018eb66..e788eeb 100644 --- a/src/agility/resources/knowledge_bases/sources/documents.py +++ b/src/agility/resources/knowledge_bases/sources/documents.py @@ -5,10 +5,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 from ...._compat import cached_property from ...._resource import SyncAPIResource, AsyncAPIResource from ...._response import ( @@ -17,10 +14,10 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from ...._base_client import make_request_options +from ....pagination import SyncMyOffsetPage, AsyncMyOffsetPage +from ...._base_client import AsyncPaginator, make_request_options from ....types.knowledge_bases.sources import document_list_params from ....types.knowledge_bases.sources.document import Document -from ....types.knowledge_bases.sources.document_list_response import DocumentListResponse __all__ = ["DocumentsResource", "AsyncDocumentsResource"] @@ -97,7 +94,7 @@ def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> DocumentListResponse: + ) -> SyncMyOffsetPage[Document]: """ List all documents for a knowledge base. @@ -114,8 +111,9 @@ def list( raise ValueError(f"Expected a non-empty value for `knowledge_base_id` but received {knowledge_base_id!r}") if not source_id: raise ValueError(f"Expected a non-empty value for `source_id` but received {source_id!r}") - return self._get( + return self._get_api_list( f"/api/knowledge_bases/{knowledge_base_id}/sources/{source_id}/documents/", + page=SyncMyOffsetPage[Document], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -129,7 +127,7 @@ def list( document_list_params.DocumentListParams, ), ), - cast_to=DocumentListResponse, + model=Document, ) @@ -192,7 +190,7 @@ async def retrieve( cast_to=Document, ) - async def list( + def list( self, source_id: str, *, @@ -205,7 +203,7 @@ async def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> DocumentListResponse: + ) -> AsyncPaginator[Document, AsyncMyOffsetPage[Document]]: """ List all documents for a knowledge base. @@ -222,14 +220,15 @@ async def list( raise ValueError(f"Expected a non-empty value for `knowledge_base_id` but received {knowledge_base_id!r}") if not source_id: raise ValueError(f"Expected a non-empty value for `source_id` but received {source_id!r}") - return await self._get( + return self._get_api_list( f"/api/knowledge_bases/{knowledge_base_id}/sources/{source_id}/documents/", + page=AsyncMyOffsetPage[Document], 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, @@ -237,7 +236,7 @@ async def list( document_list_params.DocumentListParams, ), ), - cast_to=DocumentListResponse, + model=Document, ) diff --git a/src/agility/resources/knowledge_bases/sources/sources.py b/src/agility/resources/knowledge_bases/sources/sources.py index b509b33..93f8987 100644 --- a/src/agility/resources/knowledge_bases/sources/sources.py +++ b/src/agility/resources/knowledge_bases/sources/sources.py @@ -25,10 +25,10 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from ...._base_client import make_request_options +from ....pagination import SyncMyOffsetPage, AsyncMyOffsetPage +from ...._base_client import AsyncPaginator, make_request_options from ....types.knowledge_bases import source_list_params, source_create_params, source_update_params from ....types.knowledge_bases.source import Source -from ....types.knowledge_bases.source_list_response import SourceListResponse from ....types.knowledge_bases.source_status_response import SourceStatusResponse __all__ = ["SourcesResource", "AsyncSourcesResource"] @@ -219,7 +219,7 @@ def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SourceListResponse: + ) -> SyncMyOffsetPage[Source]: """ Get all sources for a knowledge base. @@ -234,8 +234,9 @@ def list( """ if not knowledge_base_id: raise ValueError(f"Expected a non-empty value for `knowledge_base_id` but received {knowledge_base_id!r}") - return self._get( + return self._get_api_list( f"/api/knowledge_bases/{knowledge_base_id}/sources/", + page=SyncMyOffsetPage[Source], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -249,7 +250,7 @@ def list( source_list_params.SourceListParams, ), ), - cast_to=SourceListResponse, + model=Source, ) def delete( @@ -535,7 +536,7 @@ async def update( cast_to=Source, ) - async def list( + def list( self, knowledge_base_id: str, *, @@ -547,7 +548,7 @@ async def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SourceListResponse: + ) -> AsyncPaginator[Source, AsyncMyOffsetPage[Source]]: """ Get all sources for a knowledge base. @@ -562,14 +563,15 @@ async def list( """ if not knowledge_base_id: raise ValueError(f"Expected a non-empty value for `knowledge_base_id` but received {knowledge_base_id!r}") - return await self._get( + return self._get_api_list( f"/api/knowledge_bases/{knowledge_base_id}/sources/", + page=AsyncMyOffsetPage[Source], 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, @@ -577,7 +579,7 @@ async def list( source_list_params.SourceListParams, ), ), - cast_to=SourceListResponse, + model=Source, ) async def delete( diff --git a/src/agility/resources/threads/messages.py b/src/agility/resources/threads/messages.py index 1ebab9e..2b7d7f3 100644 --- a/src/agility/resources/threads/messages.py +++ b/src/agility/resources/threads/messages.py @@ -20,10 +20,10 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from ..._base_client import make_request_options +from ...pagination import SyncMyOffsetPage, AsyncMyOffsetPage +from ..._base_client import AsyncPaginator, make_request_options from ...types.threads import message_list_params, message_create_params from ...types.threads.message import Message -from ...types.threads.message_list_response import MessageListResponse __all__ = ["MessagesResource", "AsyncMessagesResource"] @@ -140,7 +140,7 @@ def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> MessageListResponse: + ) -> SyncMyOffsetPage[Message]: """ Lists messages for a given thread. @@ -155,8 +155,9 @@ def list( """ if not thread_id: raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") - return self._get( + return self._get_api_list( f"/api/threads/{thread_id}/messages/", + page=SyncMyOffsetPage[Message], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -170,7 +171,7 @@ def list( message_list_params.MessageListParams, ), ), - cast_to=MessageListResponse, + model=Message, ) def delete( @@ -311,7 +312,7 @@ async def retrieve( cast_to=Message, ) - async def list( + def list( self, thread_id: str, *, @@ -323,7 +324,7 @@ async def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> MessageListResponse: + ) -> AsyncPaginator[Message, AsyncMyOffsetPage[Message]]: """ Lists messages for a given thread. @@ -338,14 +339,15 @@ async def list( """ if not thread_id: raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") - return await self._get( + return self._get_api_list( f"/api/threads/{thread_id}/messages/", + page=AsyncMyOffsetPage[Message], 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, @@ -353,7 +355,7 @@ async def list( message_list_params.MessageListParams, ), ), - cast_to=MessageListResponse, + model=Message, ) async def delete( diff --git a/src/agility/resources/threads/threads.py b/src/agility/resources/threads/threads.py index d0c2165..6d57caa 100644 --- a/src/agility/resources/threads/threads.py +++ b/src/agility/resources/threads/threads.py @@ -14,10 +14,7 @@ ) from ...types import thread_list_params from ..._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven -from ..._utils import ( - maybe_transform, - async_maybe_transform, -) +from ..._utils import maybe_transform from .messages import ( MessagesResource, AsyncMessagesResource, @@ -34,9 +31,9 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from ..._base_client import make_request_options +from ...pagination import SyncMyOffsetPage, AsyncMyOffsetPage +from ..._base_client import AsyncPaginator, make_request_options from ...types.thread import Thread -from ...types.thread_list_response import ThreadListResponse __all__ = ["ThreadsResource", "AsyncThreadsResource"] @@ -132,7 +129,7 @@ def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> ThreadListResponse: + ) -> SyncMyOffsetPage[Thread]: """ List all threads for a user. @@ -145,8 +142,9 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ - return self._get( + return self._get_api_list( "/api/threads/", + page=SyncMyOffsetPage[Thread], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -160,7 +158,7 @@ def list( thread_list_params.ThreadListParams, ), ), - cast_to=ThreadListResponse, + model=Thread, ) def delete( @@ -278,7 +276,7 @@ async def retrieve( cast_to=Thread, ) - async def list( + def list( self, *, limit: int | NotGiven = NOT_GIVEN, @@ -289,7 +287,7 @@ async def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> ThreadListResponse: + ) -> AsyncPaginator[Thread, AsyncMyOffsetPage[Thread]]: """ List all threads for a user. @@ -302,14 +300,15 @@ async def list( timeout: Override the client-level default timeout for this request, in seconds """ - return await self._get( + return self._get_api_list( "/api/threads/", + page=AsyncMyOffsetPage[Thread], 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, @@ -317,7 +316,7 @@ async def list( thread_list_params.ThreadListParams, ), ), - cast_to=ThreadListResponse, + model=Thread, ) async def delete( diff --git a/src/agility/types/__init__.py b/src/agility/types/__init__.py index 791d45a..c93640a 100644 --- a/src/agility/types/__init__.py +++ b/src/agility/types/__init__.py @@ -6,15 +6,11 @@ from .thread import Thread as Thread from .assistant import Assistant as Assistant from .thread_list_params import ThreadListParams as ThreadListParams -from .thread_list_response import ThreadListResponse as ThreadListResponse from .assistant_list_params import AssistantListParams as AssistantListParams from .assistant_with_config import AssistantWithConfig as AssistantWithConfig -from .health_check_response import HealthCheckResponse as HealthCheckResponse from .assistant_create_params import AssistantCreateParams as AssistantCreateParams -from .assistant_list_response import AssistantListResponse as AssistantListResponse from .assistant_update_params import AssistantUpdateParams as AssistantUpdateParams from .knowledge_base_list_params import KnowledgeBaseListParams as KnowledgeBaseListParams from .knowledge_base_with_config import KnowledgeBaseWithConfig as KnowledgeBaseWithConfig from .knowledge_base_create_params import KnowledgeBaseCreateParams as KnowledgeBaseCreateParams -from .knowledge_base_list_response import KnowledgeBaseListResponse as KnowledgeBaseListResponse from .knowledge_base_update_params import KnowledgeBaseUpdateParams as KnowledgeBaseUpdateParams diff --git a/src/agility/types/assistant_list_response.py b/src/agility/types/assistant_list_response.py deleted file mode 100644 index 522859c..0000000 --- a/src/agility/types/assistant_list_response.py +++ /dev/null @@ -1,10 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List -from typing_extensions import TypeAlias - -from .assistant_with_config import AssistantWithConfig - -__all__ = ["AssistantListResponse"] - -AssistantListResponse: TypeAlias = List[AssistantWithConfig] diff --git a/src/agility/types/assistants/__init__.py b/src/agility/types/assistants/__init__.py index cf947bf..97c3a3a 100644 --- a/src/agility/types/assistants/__init__.py +++ b/src/agility/types/assistants/__init__.py @@ -5,4 +5,3 @@ from .access_key import AccessKey as AccessKey from .access_key_list_params import AccessKeyListParams as AccessKeyListParams from .access_key_create_params import AccessKeyCreateParams as AccessKeyCreateParams -from .access_key_list_response import AccessKeyListResponse as AccessKeyListResponse diff --git a/src/agility/types/assistants/access_key_list_response.py b/src/agility/types/assistants/access_key_list_response.py deleted file mode 100644 index 0a33333..0000000 --- a/src/agility/types/assistants/access_key_list_response.py +++ /dev/null @@ -1,10 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List -from typing_extensions import TypeAlias - -from .access_key import AccessKey - -__all__ = ["AccessKeyListResponse"] - -AccessKeyListResponse: TypeAlias = List[AccessKey] diff --git a/src/agility/types/health_check_response.py b/src/agility/types/health_check_response.py deleted file mode 100644 index 7a2655d..0000000 --- a/src/agility/types/health_check_response.py +++ /dev/null @@ -1,10 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - - -from .._models import BaseModel - -__all__ = ["HealthCheckResponse"] - - -class HealthCheckResponse(BaseModel): - status: str diff --git a/src/agility/types/knowledge_base_list_response.py b/src/agility/types/knowledge_base_list_response.py deleted file mode 100644 index b1ef591..0000000 --- a/src/agility/types/knowledge_base_list_response.py +++ /dev/null @@ -1,10 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List -from typing_extensions import TypeAlias - -from .knowledge_base_with_config import KnowledgeBaseWithConfig - -__all__ = ["KnowledgeBaseListResponse"] - -KnowledgeBaseListResponse: TypeAlias = List[KnowledgeBaseWithConfig] diff --git a/src/agility/types/knowledge_bases/__init__.py b/src/agility/types/knowledge_bases/__init__.py index dffb0ea..bc0ab7e 100644 --- a/src/agility/types/knowledge_bases/__init__.py +++ b/src/agility/types/knowledge_bases/__init__.py @@ -5,6 +5,5 @@ from .source import Source as Source from .source_list_params import SourceListParams as SourceListParams from .source_create_params import SourceCreateParams as SourceCreateParams -from .source_list_response import SourceListResponse as SourceListResponse from .source_update_params import SourceUpdateParams as SourceUpdateParams from .source_status_response import SourceStatusResponse as SourceStatusResponse diff --git a/src/agility/types/knowledge_bases/source_list_response.py b/src/agility/types/knowledge_bases/source_list_response.py deleted file mode 100644 index 3029f00..0000000 --- a/src/agility/types/knowledge_bases/source_list_response.py +++ /dev/null @@ -1,10 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List -from typing_extensions import TypeAlias - -from .source import Source - -__all__ = ["SourceListResponse"] - -SourceListResponse: TypeAlias = List[Source] diff --git a/src/agility/types/knowledge_bases/sources/__init__.py b/src/agility/types/knowledge_bases/sources/__init__.py index 6ae1117..9a786d0 100644 --- a/src/agility/types/knowledge_bases/sources/__init__.py +++ b/src/agility/types/knowledge_bases/sources/__init__.py @@ -4,4 +4,3 @@ from .document import Document as Document from .document_list_params import DocumentListParams as DocumentListParams -from .document_list_response import DocumentListResponse as DocumentListResponse diff --git a/src/agility/types/knowledge_bases/sources/document_list_response.py b/src/agility/types/knowledge_bases/sources/document_list_response.py deleted file mode 100644 index 5dd42b3..0000000 --- a/src/agility/types/knowledge_bases/sources/document_list_response.py +++ /dev/null @@ -1,10 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List -from typing_extensions import TypeAlias - -from .document import Document - -__all__ = ["DocumentListResponse"] - -DocumentListResponse: TypeAlias = List[Document] diff --git a/src/agility/types/thread_list_response.py b/src/agility/types/thread_list_response.py deleted file mode 100644 index bdd5254..0000000 --- a/src/agility/types/thread_list_response.py +++ /dev/null @@ -1,10 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List -from typing_extensions import TypeAlias - -from .thread import Thread - -__all__ = ["ThreadListResponse"] - -ThreadListResponse: TypeAlias = List[Thread] diff --git a/src/agility/types/threads/__init__.py b/src/agility/types/threads/__init__.py index 6b46983..adc3a89 100644 --- a/src/agility/types/threads/__init__.py +++ b/src/agility/types/threads/__init__.py @@ -8,4 +8,3 @@ from .run_stream_params import RunStreamParams as RunStreamParams from .message_list_params import MessageListParams as MessageListParams from .message_create_params import MessageCreateParams as MessageCreateParams -from .message_list_response import MessageListResponse as MessageListResponse diff --git a/src/agility/types/threads/message_list_response.py b/src/agility/types/threads/message_list_response.py deleted file mode 100644 index b316d11..0000000 --- a/src/agility/types/threads/message_list_response.py +++ /dev/null @@ -1,10 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List -from typing_extensions import TypeAlias - -from .message import Message - -__all__ = ["MessageListResponse"] - -MessageListResponse: TypeAlias = List[Message] diff --git a/tests/api_resources/assistants/test_access_keys.py b/tests/api_resources/assistants/test_access_keys.py index 0a2ffc0..00774cb 100644 --- a/tests/api_resources/assistants/test_access_keys.py +++ b/tests/api_resources/assistants/test_access_keys.py @@ -10,7 +10,8 @@ from agility import Agility, AsyncAgility from tests.utils import assert_matches_type from agility._utils import parse_datetime -from agility.types.assistants import AccessKey, AccessKeyListResponse +from agility.pagination import SyncMyOffsetPage, AsyncMyOffsetPage +from agility.types.assistants import AccessKey base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -70,60 +71,12 @@ def test_path_params_create(self, client: Agility) -> None: name="name", ) - @parametrize - def test_method_retrieve(self, client: Agility) -> None: - access_key = client.assistants.access_keys.retrieve( - access_key_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - assistant_id="assistant_id", - ) - assert_matches_type(AccessKey, access_key, path=["response"]) - - @parametrize - def test_raw_response_retrieve(self, client: Agility) -> None: - response = client.assistants.access_keys.with_raw_response.retrieve( - access_key_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - assistant_id="assistant_id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - access_key = response.parse() - assert_matches_type(AccessKey, access_key, path=["response"]) - - @parametrize - def test_streaming_response_retrieve(self, client: Agility) -> None: - with client.assistants.access_keys.with_streaming_response.retrieve( - access_key_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - assistant_id="assistant_id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - access_key = response.parse() - assert_matches_type(AccessKey, access_key, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @parametrize - def test_path_params_retrieve(self, client: Agility) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `assistant_id` but received ''"): - client.assistants.access_keys.with_raw_response.retrieve( - access_key_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - assistant_id="", - ) - - with pytest.raises(ValueError, match=r"Expected a non-empty value for `access_key_id` but received ''"): - client.assistants.access_keys.with_raw_response.retrieve( - access_key_id="", - assistant_id="assistant_id", - ) - @parametrize def test_method_list(self, client: Agility) -> None: access_key = client.assistants.access_keys.list( assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) - assert_matches_type(AccessKeyListResponse, access_key, path=["response"]) + assert_matches_type(SyncMyOffsetPage[AccessKey], access_key, path=["response"]) @parametrize def test_method_list_with_all_params(self, client: Agility) -> None: @@ -132,7 +85,7 @@ def test_method_list_with_all_params(self, client: Agility) -> None: limit=1, offset=0, ) - assert_matches_type(AccessKeyListResponse, access_key, path=["response"]) + assert_matches_type(SyncMyOffsetPage[AccessKey], access_key, path=["response"]) @parametrize def test_raw_response_list(self, client: Agility) -> None: @@ -143,7 +96,7 @@ def test_raw_response_list(self, client: Agility) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" access_key = response.parse() - assert_matches_type(AccessKeyListResponse, access_key, path=["response"]) + assert_matches_type(SyncMyOffsetPage[AccessKey], access_key, path=["response"]) @parametrize def test_streaming_response_list(self, client: Agility) -> None: @@ -154,7 +107,7 @@ def test_streaming_response_list(self, client: Agility) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" access_key = response.parse() - assert_matches_type(AccessKeyListResponse, access_key, path=["response"]) + assert_matches_type(SyncMyOffsetPage[AccessKey], access_key, path=["response"]) assert cast(Any, response.is_closed) is True @@ -221,60 +174,12 @@ async def test_path_params_create(self, async_client: AsyncAgility) -> None: name="name", ) - @parametrize - async def test_method_retrieve(self, async_client: AsyncAgility) -> None: - access_key = await async_client.assistants.access_keys.retrieve( - access_key_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - assistant_id="assistant_id", - ) - assert_matches_type(AccessKey, access_key, path=["response"]) - - @parametrize - async def test_raw_response_retrieve(self, async_client: AsyncAgility) -> None: - response = await async_client.assistants.access_keys.with_raw_response.retrieve( - access_key_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - assistant_id="assistant_id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - access_key = await response.parse() - assert_matches_type(AccessKey, access_key, path=["response"]) - - @parametrize - async def test_streaming_response_retrieve(self, async_client: AsyncAgility) -> None: - async with async_client.assistants.access_keys.with_streaming_response.retrieve( - access_key_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - assistant_id="assistant_id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - access_key = await response.parse() - assert_matches_type(AccessKey, access_key, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @parametrize - async def test_path_params_retrieve(self, async_client: AsyncAgility) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `assistant_id` but received ''"): - await async_client.assistants.access_keys.with_raw_response.retrieve( - access_key_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - assistant_id="", - ) - - with pytest.raises(ValueError, match=r"Expected a non-empty value for `access_key_id` but received ''"): - await async_client.assistants.access_keys.with_raw_response.retrieve( - access_key_id="", - assistant_id="assistant_id", - ) - @parametrize async def test_method_list(self, async_client: AsyncAgility) -> None: access_key = await async_client.assistants.access_keys.list( assistant_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) - assert_matches_type(AccessKeyListResponse, access_key, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[AccessKey], access_key, path=["response"]) @parametrize async def test_method_list_with_all_params(self, async_client: AsyncAgility) -> None: @@ -283,7 +188,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncAgility) -> limit=1, offset=0, ) - assert_matches_type(AccessKeyListResponse, access_key, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[AccessKey], access_key, path=["response"]) @parametrize async def test_raw_response_list(self, async_client: AsyncAgility) -> None: @@ -294,7 +199,7 @@ async def test_raw_response_list(self, async_client: AsyncAgility) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" access_key = await response.parse() - assert_matches_type(AccessKeyListResponse, access_key, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[AccessKey], access_key, path=["response"]) @parametrize async def test_streaming_response_list(self, async_client: AsyncAgility) -> None: @@ -305,7 +210,7 @@ async def test_streaming_response_list(self, async_client: AsyncAgility) -> None assert response.http_request.headers.get("X-Stainless-Lang") == "python" access_key = await response.parse() - assert_matches_type(AccessKeyListResponse, access_key, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[AccessKey], access_key, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/knowledge_bases/sources/test_documents.py b/tests/api_resources/knowledge_bases/sources/test_documents.py index 84fbe9a..507f61a 100644 --- a/tests/api_resources/knowledge_bases/sources/test_documents.py +++ b/tests/api_resources/knowledge_bases/sources/test_documents.py @@ -9,7 +9,8 @@ from agility import Agility, AsyncAgility from tests.utils import assert_matches_type -from agility.types.knowledge_bases.sources import Document, DocumentListResponse +from agility.pagination import SyncMyOffsetPage, AsyncMyOffsetPage +from agility.types.knowledge_bases.sources import Document base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -83,7 +84,7 @@ def test_method_list(self, client: Agility) -> None: source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) - assert_matches_type(DocumentListResponse, document, path=["response"]) + assert_matches_type(SyncMyOffsetPage[Document], document, path=["response"]) @parametrize def test_method_list_with_all_params(self, client: Agility) -> None: @@ -93,7 +94,7 @@ def test_method_list_with_all_params(self, client: Agility) -> None: limit=1, offset=0, ) - assert_matches_type(DocumentListResponse, document, path=["response"]) + assert_matches_type(SyncMyOffsetPage[Document], document, path=["response"]) @parametrize def test_raw_response_list(self, client: Agility) -> None: @@ -105,7 +106,7 @@ def test_raw_response_list(self, client: Agility) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" document = response.parse() - assert_matches_type(DocumentListResponse, document, path=["response"]) + assert_matches_type(SyncMyOffsetPage[Document], document, path=["response"]) @parametrize def test_streaming_response_list(self, client: Agility) -> None: @@ -117,7 +118,7 @@ def test_streaming_response_list(self, client: Agility) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" document = response.parse() - assert_matches_type(DocumentListResponse, document, path=["response"]) + assert_matches_type(SyncMyOffsetPage[Document], document, path=["response"]) assert cast(Any, response.is_closed) is True @@ -205,7 +206,7 @@ async def test_method_list(self, async_client: AsyncAgility) -> None: source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) - assert_matches_type(DocumentListResponse, document, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[Document], document, path=["response"]) @parametrize async def test_method_list_with_all_params(self, async_client: AsyncAgility) -> None: @@ -215,7 +216,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncAgility) -> limit=1, offset=0, ) - assert_matches_type(DocumentListResponse, document, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[Document], document, path=["response"]) @parametrize async def test_raw_response_list(self, async_client: AsyncAgility) -> None: @@ -227,7 +228,7 @@ async def test_raw_response_list(self, async_client: AsyncAgility) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" document = await response.parse() - assert_matches_type(DocumentListResponse, document, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[Document], document, path=["response"]) @parametrize async def test_streaming_response_list(self, async_client: AsyncAgility) -> None: @@ -239,7 +240,7 @@ async def test_streaming_response_list(self, async_client: AsyncAgility) -> None assert response.http_request.headers.get("X-Stainless-Lang") == "python" document = await response.parse() - assert_matches_type(DocumentListResponse, document, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[Document], document, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/knowledge_bases/test_sources.py b/tests/api_resources/knowledge_bases/test_sources.py index bf69823..0446b1e 100644 --- a/tests/api_resources/knowledge_bases/test_sources.py +++ b/tests/api_resources/knowledge_bases/test_sources.py @@ -9,9 +9,9 @@ from agility import Agility, AsyncAgility from tests.utils import assert_matches_type +from agility.pagination import SyncMyOffsetPage, AsyncMyOffsetPage from agility.types.knowledge_bases import ( Source, - SourceListResponse, SourceStatusResponse, ) @@ -272,7 +272,7 @@ def test_method_list(self, client: Agility) -> None: source = client.knowledge_bases.sources.list( knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) - assert_matches_type(SourceListResponse, source, path=["response"]) + assert_matches_type(SyncMyOffsetPage[Source], source, path=["response"]) @parametrize def test_method_list_with_all_params(self, client: Agility) -> None: @@ -281,7 +281,7 @@ def test_method_list_with_all_params(self, client: Agility) -> None: limit=1, offset=0, ) - assert_matches_type(SourceListResponse, source, path=["response"]) + assert_matches_type(SyncMyOffsetPage[Source], source, path=["response"]) @parametrize def test_raw_response_list(self, client: Agility) -> None: @@ -292,7 +292,7 @@ def test_raw_response_list(self, client: Agility) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" source = response.parse() - assert_matches_type(SourceListResponse, source, path=["response"]) + assert_matches_type(SyncMyOffsetPage[Source], source, path=["response"]) @parametrize def test_streaming_response_list(self, client: Agility) -> None: @@ -303,7 +303,7 @@ def test_streaming_response_list(self, client: Agility) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" source = response.parse() - assert_matches_type(SourceListResponse, source, path=["response"]) + assert_matches_type(SyncMyOffsetPage[Source], source, path=["response"]) assert cast(Any, response.is_closed) is True @@ -713,7 +713,7 @@ async def test_method_list(self, async_client: AsyncAgility) -> None: source = await async_client.knowledge_bases.sources.list( knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) - assert_matches_type(SourceListResponse, source, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[Source], source, path=["response"]) @parametrize async def test_method_list_with_all_params(self, async_client: AsyncAgility) -> None: @@ -722,7 +722,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncAgility) -> limit=1, offset=0, ) - assert_matches_type(SourceListResponse, source, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[Source], source, path=["response"]) @parametrize async def test_raw_response_list(self, async_client: AsyncAgility) -> None: @@ -733,7 +733,7 @@ async def test_raw_response_list(self, async_client: AsyncAgility) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" source = await response.parse() - assert_matches_type(SourceListResponse, source, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[Source], source, path=["response"]) @parametrize async def test_streaming_response_list(self, async_client: AsyncAgility) -> None: @@ -744,7 +744,7 @@ async def test_streaming_response_list(self, async_client: AsyncAgility) -> None assert response.http_request.headers.get("X-Stainless-Lang") == "python" source = await response.parse() - assert_matches_type(SourceListResponse, source, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[Source], source, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_assistants.py b/tests/api_resources/test_assistants.py index bc9403b..27cdc31 100644 --- a/tests/api_resources/test_assistants.py +++ b/tests/api_resources/test_assistants.py @@ -12,8 +12,8 @@ from agility.types import ( Assistant, AssistantWithConfig, - AssistantListResponse, ) +from agility.pagination import SyncMyOffsetPage, AsyncMyOffsetPage base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -181,7 +181,7 @@ def test_path_params_update(self, client: Agility) -> None: @parametrize def test_method_list(self, client: Agility) -> None: assistant = client.assistants.list() - assert_matches_type(AssistantListResponse, assistant, path=["response"]) + assert_matches_type(SyncMyOffsetPage[AssistantWithConfig], assistant, path=["response"]) @parametrize def test_method_list_with_all_params(self, client: Agility) -> None: @@ -189,7 +189,7 @@ def test_method_list_with_all_params(self, client: Agility) -> None: limit=1, offset=0, ) - assert_matches_type(AssistantListResponse, assistant, path=["response"]) + assert_matches_type(SyncMyOffsetPage[AssistantWithConfig], assistant, path=["response"]) @parametrize def test_raw_response_list(self, client: Agility) -> None: @@ -198,7 +198,7 @@ def test_raw_response_list(self, client: Agility) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" assistant = response.parse() - assert_matches_type(AssistantListResponse, assistant, path=["response"]) + assert_matches_type(SyncMyOffsetPage[AssistantWithConfig], assistant, path=["response"]) @parametrize def test_streaming_response_list(self, client: Agility) -> None: @@ -207,7 +207,7 @@ def test_streaming_response_list(self, client: Agility) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" assistant = response.parse() - assert_matches_type(AssistantListResponse, assistant, path=["response"]) + assert_matches_type(SyncMyOffsetPage[AssistantWithConfig], assistant, path=["response"]) assert cast(Any, response.is_closed) is True @@ -413,7 +413,7 @@ async def test_path_params_update(self, async_client: AsyncAgility) -> None: @parametrize async def test_method_list(self, async_client: AsyncAgility) -> None: assistant = await async_client.assistants.list() - assert_matches_type(AssistantListResponse, assistant, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[AssistantWithConfig], assistant, path=["response"]) @parametrize async def test_method_list_with_all_params(self, async_client: AsyncAgility) -> None: @@ -421,7 +421,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncAgility) -> limit=1, offset=0, ) - assert_matches_type(AssistantListResponse, assistant, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[AssistantWithConfig], assistant, path=["response"]) @parametrize async def test_raw_response_list(self, async_client: AsyncAgility) -> None: @@ -430,7 +430,7 @@ async def test_raw_response_list(self, async_client: AsyncAgility) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" assistant = await response.parse() - assert_matches_type(AssistantListResponse, assistant, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[AssistantWithConfig], assistant, path=["response"]) @parametrize async def test_streaming_response_list(self, async_client: AsyncAgility) -> None: @@ -439,7 +439,7 @@ async def test_streaming_response_list(self, async_client: AsyncAgility) -> None assert response.http_request.headers.get("X-Stainless-Lang") == "python" assistant = await response.parse() - assert_matches_type(AssistantListResponse, assistant, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[AssistantWithConfig], assistant, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_health.py b/tests/api_resources/test_health.py deleted file mode 100644 index c448a14..0000000 --- a/tests/api_resources/test_health.py +++ /dev/null @@ -1,72 +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 agility import Agility, AsyncAgility -from tests.utils import assert_matches_type -from agility.types import HealthCheckResponse - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestHealth: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @parametrize - def test_method_check(self, client: Agility) -> None: - health = client.health.check() - assert_matches_type(HealthCheckResponse, health, path=["response"]) - - @parametrize - def test_raw_response_check(self, client: Agility) -> None: - response = client.health.with_raw_response.check() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - health = response.parse() - assert_matches_type(HealthCheckResponse, health, path=["response"]) - - @parametrize - def test_streaming_response_check(self, client: Agility) -> None: - with client.health.with_streaming_response.check() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - health = response.parse() - assert_matches_type(HealthCheckResponse, health, path=["response"]) - - assert cast(Any, response.is_closed) is True - - -class TestAsyncHealth: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) - - @parametrize - async def test_method_check(self, async_client: AsyncAgility) -> None: - health = await async_client.health.check() - assert_matches_type(HealthCheckResponse, health, path=["response"]) - - @parametrize - async def test_raw_response_check(self, async_client: AsyncAgility) -> None: - response = await async_client.health.with_raw_response.check() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - health = await response.parse() - assert_matches_type(HealthCheckResponse, health, path=["response"]) - - @parametrize - async def test_streaming_response_check(self, async_client: AsyncAgility) -> None: - async with async_client.health.with_streaming_response.check() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - health = await response.parse() - assert_matches_type(HealthCheckResponse, health, path=["response"]) - - assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_knowledge_bases.py b/tests/api_resources/test_knowledge_bases.py index 9cd2110..3726d9e 100644 --- a/tests/api_resources/test_knowledge_bases.py +++ b/tests/api_resources/test_knowledge_bases.py @@ -11,8 +11,8 @@ from tests.utils import assert_matches_type from agility.types import ( KnowledgeBaseWithConfig, - KnowledgeBaseListResponse, ) +from agility.pagination import SyncMyOffsetPage, AsyncMyOffsetPage base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -183,7 +183,7 @@ def test_path_params_update(self, client: Agility) -> None: @parametrize def test_method_list(self, client: Agility) -> None: knowledge_base = client.knowledge_bases.list() - assert_matches_type(KnowledgeBaseListResponse, knowledge_base, path=["response"]) + assert_matches_type(SyncMyOffsetPage[KnowledgeBaseWithConfig], knowledge_base, path=["response"]) @parametrize def test_method_list_with_all_params(self, client: Agility) -> None: @@ -191,7 +191,7 @@ def test_method_list_with_all_params(self, client: Agility) -> None: limit=1, offset=0, ) - assert_matches_type(KnowledgeBaseListResponse, knowledge_base, path=["response"]) + assert_matches_type(SyncMyOffsetPage[KnowledgeBaseWithConfig], knowledge_base, path=["response"]) @parametrize def test_raw_response_list(self, client: Agility) -> None: @@ -200,7 +200,7 @@ def test_raw_response_list(self, client: Agility) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" knowledge_base = response.parse() - assert_matches_type(KnowledgeBaseListResponse, knowledge_base, path=["response"]) + assert_matches_type(SyncMyOffsetPage[KnowledgeBaseWithConfig], knowledge_base, path=["response"]) @parametrize def test_streaming_response_list(self, client: Agility) -> None: @@ -209,7 +209,7 @@ def test_streaming_response_list(self, client: Agility) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" knowledge_base = response.parse() - assert_matches_type(KnowledgeBaseListResponse, knowledge_base, path=["response"]) + assert_matches_type(SyncMyOffsetPage[KnowledgeBaseWithConfig], knowledge_base, path=["response"]) assert cast(Any, response.is_closed) is True @@ -418,7 +418,7 @@ async def test_path_params_update(self, async_client: AsyncAgility) -> None: @parametrize async def test_method_list(self, async_client: AsyncAgility) -> None: knowledge_base = await async_client.knowledge_bases.list() - assert_matches_type(KnowledgeBaseListResponse, knowledge_base, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[KnowledgeBaseWithConfig], knowledge_base, path=["response"]) @parametrize async def test_method_list_with_all_params(self, async_client: AsyncAgility) -> None: @@ -426,7 +426,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncAgility) -> limit=1, offset=0, ) - assert_matches_type(KnowledgeBaseListResponse, knowledge_base, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[KnowledgeBaseWithConfig], knowledge_base, path=["response"]) @parametrize async def test_raw_response_list(self, async_client: AsyncAgility) -> None: @@ -435,7 +435,7 @@ async def test_raw_response_list(self, async_client: AsyncAgility) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" knowledge_base = await response.parse() - assert_matches_type(KnowledgeBaseListResponse, knowledge_base, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[KnowledgeBaseWithConfig], knowledge_base, path=["response"]) @parametrize async def test_streaming_response_list(self, async_client: AsyncAgility) -> None: @@ -444,7 +444,7 @@ async def test_streaming_response_list(self, async_client: AsyncAgility) -> None assert response.http_request.headers.get("X-Stainless-Lang") == "python" knowledge_base = await response.parse() - assert_matches_type(KnowledgeBaseListResponse, knowledge_base, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[KnowledgeBaseWithConfig], knowledge_base, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_threads.py b/tests/api_resources/test_threads.py index 250a6dd..c83c69a 100644 --- a/tests/api_resources/test_threads.py +++ b/tests/api_resources/test_threads.py @@ -9,7 +9,8 @@ from agility import Agility, AsyncAgility from tests.utils import assert_matches_type -from agility.types import Thread, ThreadListResponse +from agility.types import Thread +from agility.pagination import SyncMyOffsetPage, AsyncMyOffsetPage base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -83,7 +84,7 @@ def test_path_params_retrieve(self, client: Agility) -> None: @parametrize def test_method_list(self, client: Agility) -> None: thread = client.threads.list() - assert_matches_type(ThreadListResponse, thread, path=["response"]) + assert_matches_type(SyncMyOffsetPage[Thread], thread, path=["response"]) @parametrize def test_method_list_with_all_params(self, client: Agility) -> None: @@ -91,7 +92,7 @@ def test_method_list_with_all_params(self, client: Agility) -> None: limit=1, offset=0, ) - assert_matches_type(ThreadListResponse, thread, path=["response"]) + assert_matches_type(SyncMyOffsetPage[Thread], thread, path=["response"]) @parametrize def test_raw_response_list(self, client: Agility) -> None: @@ -100,7 +101,7 @@ def test_raw_response_list(self, client: Agility) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" thread = response.parse() - assert_matches_type(ThreadListResponse, thread, path=["response"]) + assert_matches_type(SyncMyOffsetPage[Thread], thread, path=["response"]) @parametrize def test_streaming_response_list(self, client: Agility) -> None: @@ -109,7 +110,7 @@ def test_streaming_response_list(self, client: Agility) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" thread = response.parse() - assert_matches_type(ThreadListResponse, thread, path=["response"]) + assert_matches_type(SyncMyOffsetPage[Thread], thread, path=["response"]) assert cast(Any, response.is_closed) is True @@ -221,7 +222,7 @@ async def test_path_params_retrieve(self, async_client: AsyncAgility) -> None: @parametrize async def test_method_list(self, async_client: AsyncAgility) -> None: thread = await async_client.threads.list() - assert_matches_type(ThreadListResponse, thread, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[Thread], thread, path=["response"]) @parametrize async def test_method_list_with_all_params(self, async_client: AsyncAgility) -> None: @@ -229,7 +230,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncAgility) -> limit=1, offset=0, ) - assert_matches_type(ThreadListResponse, thread, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[Thread], thread, path=["response"]) @parametrize async def test_raw_response_list(self, async_client: AsyncAgility) -> None: @@ -238,7 +239,7 @@ async def test_raw_response_list(self, async_client: AsyncAgility) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" thread = await response.parse() - assert_matches_type(ThreadListResponse, thread, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[Thread], thread, path=["response"]) @parametrize async def test_streaming_response_list(self, async_client: AsyncAgility) -> None: @@ -247,7 +248,7 @@ async def test_streaming_response_list(self, async_client: AsyncAgility) -> None assert response.http_request.headers.get("X-Stainless-Lang") == "python" thread = await response.parse() - assert_matches_type(ThreadListResponse, thread, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[Thread], thread, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/threads/test_messages.py b/tests/api_resources/threads/test_messages.py index 3f449c9..1741837 100644 --- a/tests/api_resources/threads/test_messages.py +++ b/tests/api_resources/threads/test_messages.py @@ -9,7 +9,8 @@ from agility import Agility, AsyncAgility from tests.utils import assert_matches_type -from agility.types.threads import Message, MessageListResponse +from agility.pagination import SyncMyOffsetPage, AsyncMyOffsetPage +from agility.types.threads import Message base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -130,7 +131,7 @@ def test_method_list(self, client: Agility) -> None: message = client.threads.messages.list( thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) - assert_matches_type(MessageListResponse, message, path=["response"]) + assert_matches_type(SyncMyOffsetPage[Message], message, path=["response"]) @parametrize def test_method_list_with_all_params(self, client: Agility) -> None: @@ -139,7 +140,7 @@ def test_method_list_with_all_params(self, client: Agility) -> None: limit=1, offset=0, ) - assert_matches_type(MessageListResponse, message, path=["response"]) + assert_matches_type(SyncMyOffsetPage[Message], message, path=["response"]) @parametrize def test_raw_response_list(self, client: Agility) -> None: @@ -150,7 +151,7 @@ def test_raw_response_list(self, client: Agility) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" message = response.parse() - assert_matches_type(MessageListResponse, message, path=["response"]) + assert_matches_type(SyncMyOffsetPage[Message], message, path=["response"]) @parametrize def test_streaming_response_list(self, client: Agility) -> None: @@ -161,7 +162,7 @@ def test_streaming_response_list(self, client: Agility) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" message = response.parse() - assert_matches_type(MessageListResponse, message, path=["response"]) + assert_matches_type(SyncMyOffsetPage[Message], message, path=["response"]) assert cast(Any, response.is_closed) is True @@ -337,7 +338,7 @@ async def test_method_list(self, async_client: AsyncAgility) -> None: message = await async_client.threads.messages.list( thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) - assert_matches_type(MessageListResponse, message, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[Message], message, path=["response"]) @parametrize async def test_method_list_with_all_params(self, async_client: AsyncAgility) -> None: @@ -346,7 +347,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncAgility) -> limit=1, offset=0, ) - assert_matches_type(MessageListResponse, message, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[Message], message, path=["response"]) @parametrize async def test_raw_response_list(self, async_client: AsyncAgility) -> None: @@ -357,7 +358,7 @@ async def test_raw_response_list(self, async_client: AsyncAgility) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" message = await response.parse() - assert_matches_type(MessageListResponse, message, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[Message], message, path=["response"]) @parametrize async def test_streaming_response_list(self, async_client: AsyncAgility) -> None: @@ -368,7 +369,7 @@ async def test_streaming_response_list(self, async_client: AsyncAgility) -> None assert response.http_request.headers.get("X-Stainless-Lang") == "python" message = await response.parse() - assert_matches_type(MessageListResponse, message, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[Message], message, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/test_client.py b/tests/test_client.py index 21c369f..82eaed7 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -542,6 +542,14 @@ def test_base_url_env(self) -> None: client = Agility(api_key=api_key, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" + # explicit environment arg requires explicitness + with update_env(AGILITY_BASE_URL="http://localhost:5000/from/env"): + with pytest.raises(ValueError, match=r"you must pass base_url=None"): + Agility(api_key=api_key, _strict_response_validation=True, environment="production") + + client = Agility(base_url=None, api_key=api_key, _strict_response_validation=True, environment="production") + assert str(client.base_url).startswith("https://api-agility.cleanlab.ai") + @pytest.mark.parametrize( "client", [ @@ -1318,6 +1326,16 @@ def test_base_url_env(self) -> None: client = AsyncAgility(api_key=api_key, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" + # explicit environment arg requires explicitness + with update_env(AGILITY_BASE_URL="http://localhost:5000/from/env"): + with pytest.raises(ValueError, match=r"you must pass base_url=None"): + AsyncAgility(api_key=api_key, _strict_response_validation=True, environment="production") + + client = AsyncAgility( + base_url=None, api_key=api_key, _strict_response_validation=True, environment="production" + ) + assert str(client.base_url).startswith("https://api-agility.cleanlab.ai") + @pytest.mark.parametrize( "client", [ From cf1147972ada3f9bbe82b31099f51a3baae0c0e5 Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Mon, 4 Nov 2024 19:17:18 +0000 Subject: [PATCH 10/77] feat(api): api update --- .stats.yml | 2 +- README.md | 4 ++-- pyproject.toml | 5 ++--- src/agility/_compat.py | 6 ++++-- src/agility/_models.py | 9 +++++--- src/agility/_utils/__init__.py | 1 + src/agility/_utils/_transform.py | 9 ++++++-- src/agility/_utils/_utils.py | 17 +++++++++++++++ src/agility/types/knowledge_bases/source.py | 17 ++++++++++++++- .../knowledge_bases/source_create_params.py | 17 ++++++++++++++- .../knowledge_bases/source_update_params.py | 17 ++++++++++++++- src/agility/types/threads/run.py | 2 ++ tests/test_models.py | 21 +++++++------------ tests/test_transform.py | 15 +++++++++++++ 14 files changed, 112 insertions(+), 30 deletions(-) diff --git a/.stats.yml b/.stats.yml index 479db52..87a6ba5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 36 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-36e7c2cdfead9cc1628d9e75a9be97a5580130287e8610c5bceac14770743512.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-4abd340ba528b5912ad6672a203de3c6904577d27b3397533d51d3ddc265339f.yml diff --git a/README.md b/README.md index 3b8a978..64977d0 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![PyPI version](https://img.shields.io/pypi/v/agility.svg)](https://pypi.org/project/agility/) -The Agility Python library provides convenient access to the Agility REST API from any Python 3.7+ +The Agility Python library provides convenient access to the Agility 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). @@ -404,7 +404,7 @@ print(agility.__version__) ## Requirements -Python 3.7 or higher. +Python 3.8 or higher. ## Contributing diff --git a/pyproject.toml b/pyproject.toml index 915150f..0714767 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/src/agility/_compat.py b/src/agility/_compat.py index d89920d..4794129 100644 --- a/src/agility/_compat.py +++ b/src/agility/_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/agility/_models.py b/src/agility/_models.py index 42551b7..6cb469e 100644 --- a/src/agility/_models.py +++ b/src/agility/_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/agility/_utils/__init__.py b/src/agility/_utils/__init__.py index 3efe66c..a7cff3c 100644 --- a/src/agility/_utils/__init__.py +++ b/src/agility/_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/agility/_utils/_transform.py b/src/agility/_utils/_transform.py index 47e262a..d7c0534 100644 --- a/src/agility/_utils/_transform.py +++ b/src/agility/_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] @@ -186,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: @@ -324,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/agility/_utils/_utils.py b/src/agility/_utils/_utils.py index 0bba17c..e5811bb 100644 --- a/src/agility/_utils/_utils.py +++ b/src/agility/_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/src/agility/types/knowledge_bases/source.py b/src/agility/types/knowledge_bases/source.py index 83497a9..dbb3326 100644 --- a/src/agility/types/knowledge_bases/source.py +++ b/src/agility/types/knowledge_bases/source.py @@ -13,6 +13,7 @@ "SourceParamsWebV0Params", "SourceParamsNotionParams", "SourceParamsS3PublicV0Params", + "SourceParamsS3PrivateV0Params", "SourceSchedule", ] @@ -49,8 +50,22 @@ class SourceParamsS3PublicV0Params(BaseModel): name: Optional[Literal["s3_public_v0"]] = None +class SourceParamsS3PrivateV0Params(BaseModel): + bucket_name: str + + integration_id: str + + limit: int + + prefix: str + + name: Optional[Literal["s3_private_v0"]] = None + + SourceParams: TypeAlias = Annotated[ - Union[SourceParamsWebV0Params, SourceParamsNotionParams, SourceParamsS3PublicV0Params], + Union[ + SourceParamsWebV0Params, SourceParamsNotionParams, SourceParamsS3PublicV0Params, SourceParamsS3PrivateV0Params + ], PropertyInfo(discriminator="name"), ] diff --git a/src/agility/types/knowledge_bases/source_create_params.py b/src/agility/types/knowledge_bases/source_create_params.py index 0014862..03bc9a3 100644 --- a/src/agility/types/knowledge_bases/source_create_params.py +++ b/src/agility/types/knowledge_bases/source_create_params.py @@ -11,6 +11,7 @@ "SourceParamsWebV0Params", "SourceParamsNotionParams", "SourceParamsS3PublicV0Params", + "SourceParamsS3PrivateV0Params", "SourceSchedule", ] @@ -61,7 +62,21 @@ class SourceParamsS3PublicV0Params(TypedDict, total=False): name: Literal["s3_public_v0"] -SourceParams: TypeAlias = Union[SourceParamsWebV0Params, SourceParamsNotionParams, SourceParamsS3PublicV0Params] +class SourceParamsS3PrivateV0Params(TypedDict, total=False): + bucket_name: Required[str] + + integration_id: Required[str] + + limit: Required[int] + + prefix: Required[str] + + name: Literal["s3_private_v0"] + + +SourceParams: TypeAlias = Union[ + SourceParamsWebV0Params, SourceParamsNotionParams, SourceParamsS3PublicV0Params, SourceParamsS3PrivateV0Params +] class SourceSchedule(TypedDict, total=False): diff --git a/src/agility/types/knowledge_bases/source_update_params.py b/src/agility/types/knowledge_bases/source_update_params.py index bfbad51..b2cabb2 100644 --- a/src/agility/types/knowledge_bases/source_update_params.py +++ b/src/agility/types/knowledge_bases/source_update_params.py @@ -11,6 +11,7 @@ "SourceParamsWebV0Params", "SourceParamsNotionParams", "SourceParamsS3PublicV0Params", + "SourceParamsS3PrivateV0Params", "SourceSchedule", ] @@ -63,7 +64,21 @@ class SourceParamsS3PublicV0Params(TypedDict, total=False): name: Literal["s3_public_v0"] -SourceParams: TypeAlias = Union[SourceParamsWebV0Params, SourceParamsNotionParams, SourceParamsS3PublicV0Params] +class SourceParamsS3PrivateV0Params(TypedDict, total=False): + bucket_name: Required[str] + + integration_id: Required[str] + + limit: Required[int] + + prefix: Required[str] + + name: Literal["s3_private_v0"] + + +SourceParams: TypeAlias = Union[ + SourceParamsWebV0Params, SourceParamsNotionParams, SourceParamsS3PublicV0Params, SourceParamsS3PrivateV0Params +] class SourceSchedule(TypedDict, total=False): diff --git a/src/agility/types/threads/run.py b/src/agility/types/threads/run.py index 192adc5..a8ef291 100644 --- a/src/agility/types/threads/run.py +++ b/src/agility/types/threads/run.py @@ -38,6 +38,8 @@ class Run(BaseModel): knowledge_base_id: Optional[str] = None + last_error: Optional[str] = None + model: Optional[Literal["gpt-4o"]] = None usage: Optional[Usage] = None diff --git a/tests/test_models.py b/tests/test_models.py index 4ba3148..2169428 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 48460a0..bdb468b 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 f299d7e9f61886d20711301e843f7a4cbd8e91d1 Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Mon, 4 Nov 2024 19:20:49 +0000 Subject: [PATCH 11/77] feat(api): manual updates --- .stats.yml | 2 +- api.md | 40 ++ src/agility/_client.py | 8 + src/agility/resources/__init__.py | 14 + .../resources/integrations/__init__.py | 47 ++ .../resources/integrations/available.py | 135 +++++ .../resources/integrations/integrations.py | 519 ++++++++++++++++++ src/agility/resources/integrations/rbac.py | 163 ++++++ src/agility/types/__init__.py | 8 + src/agility/types/gc_sv0_integration.py | 30 + src/agility/types/integration.py | 17 + .../types/integration_create_params.py | 51 ++ .../types/integration_create_response.py | 11 + src/agility/types/integration_list_params.py | 13 + .../types/integration_list_response.py | 11 + .../types/integration_retrieve_response.py | 11 + src/agility/types/integrations/__init__.py | 6 + .../integrations/available_list_response.py | 10 + .../integrations/integration_type_def.py | 17 + src/agility/types/s3_v0_integration.py | 36 ++ tests/api_resources/integrations/__init__.py | 1 + .../integrations/test_available.py | 72 +++ tests/api_resources/integrations/test_rbac.py | 98 ++++ tests/api_resources/test_integrations.py | 367 +++++++++++++ 24 files changed, 1686 insertions(+), 1 deletion(-) create mode 100644 src/agility/resources/integrations/__init__.py create mode 100644 src/agility/resources/integrations/available.py create mode 100644 src/agility/resources/integrations/integrations.py create mode 100644 src/agility/resources/integrations/rbac.py create mode 100644 src/agility/types/gc_sv0_integration.py create mode 100644 src/agility/types/integration.py create mode 100644 src/agility/types/integration_create_params.py create mode 100644 src/agility/types/integration_create_response.py create mode 100644 src/agility/types/integration_list_params.py create mode 100644 src/agility/types/integration_list_response.py create mode 100644 src/agility/types/integration_retrieve_response.py create mode 100644 src/agility/types/integrations/__init__.py create mode 100644 src/agility/types/integrations/available_list_response.py create mode 100644 src/agility/types/integrations/integration_type_def.py create mode 100644 src/agility/types/s3_v0_integration.py create mode 100644 tests/api_resources/integrations/__init__.py create mode 100644 tests/api_resources/integrations/test_available.py create mode 100644 tests/api_resources/integrations/test_rbac.py create mode 100644 tests/api_resources/test_integrations.py diff --git a/.stats.yml b/.stats.yml index 87a6ba5..20ebe85 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ -configured_endpoints: 36 +configured_endpoints: 42 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-4abd340ba528b5912ad6672a203de3c6904577d27b3397533d51d3ddc265339f.yml diff --git a/api.md b/api.md index 7cca472..d86da6f 100644 --- a/api.md +++ b/api.md @@ -143,3 +143,43 @@ Methods: - client.threads.runs.retrieve(run_id, \*, thread_id) -> Run - client.threads.runs.delete(run_id, \*, thread_id) -> None - client.threads.runs.stream(thread_id, \*\*params) -> object + +# Integrations + +Types: + +```python +from agility.types import ( + GcSv0Integration, + Integration, + S3V0Integration, + IntegrationCreateResponse, + IntegrationRetrieveResponse, + IntegrationListResponse, +) +``` + +Methods: + +- client.integrations.create(\*\*params) -> IntegrationCreateResponse +- client.integrations.retrieve(integration_id) -> IntegrationRetrieveResponse +- client.integrations.list(\*\*params) -> SyncMyOffsetPage[IntegrationListResponse] +- client.integrations.delete(integration_id) -> None + +## Available + +Types: + +```python +from agility.types.integrations import IntegrationTypeDef, AvailableListResponse +``` + +Methods: + +- client.integrations.available.list() -> AvailableListResponse + +## Rbac + +Methods: + +- client.integrations.rbac.verify(integration_id) -> Integration diff --git a/src/agility/_client.py b/src/agility/_client.py index 4eb4597..a7dbd56 100644 --- a/src/agility/_client.py +++ b/src/agility/_client.py @@ -58,6 +58,7 @@ class Agility(SyncAPIClient): knowledge_bases: resources.KnowledgeBasesResource users: resources.UsersResource threads: resources.ThreadsResource + integrations: resources.IntegrationsResource with_raw_response: AgilityWithRawResponse with_streaming_response: AgilityWithStreamedResponse @@ -158,6 +159,7 @@ def __init__( self.knowledge_bases = resources.KnowledgeBasesResource(self) self.users = resources.UsersResource(self) self.threads = resources.ThreadsResource(self) + self.integrations = resources.IntegrationsResource(self) self.with_raw_response = AgilityWithRawResponse(self) self.with_streaming_response = AgilityWithStreamedResponse(self) @@ -301,6 +303,7 @@ class AsyncAgility(AsyncAPIClient): knowledge_bases: resources.AsyncKnowledgeBasesResource users: resources.AsyncUsersResource threads: resources.AsyncThreadsResource + integrations: resources.AsyncIntegrationsResource with_raw_response: AsyncAgilityWithRawResponse with_streaming_response: AsyncAgilityWithStreamedResponse @@ -401,6 +404,7 @@ def __init__( self.knowledge_bases = resources.AsyncKnowledgeBasesResource(self) self.users = resources.AsyncUsersResource(self) self.threads = resources.AsyncThreadsResource(self) + self.integrations = resources.AsyncIntegrationsResource(self) self.with_raw_response = AsyncAgilityWithRawResponse(self) self.with_streaming_response = AsyncAgilityWithStreamedResponse(self) @@ -545,6 +549,7 @@ def __init__(self, client: Agility) -> None: self.knowledge_bases = resources.KnowledgeBasesResourceWithRawResponse(client.knowledge_bases) self.users = resources.UsersResourceWithRawResponse(client.users) self.threads = resources.ThreadsResourceWithRawResponse(client.threads) + self.integrations = resources.IntegrationsResourceWithRawResponse(client.integrations) class AsyncAgilityWithRawResponse: @@ -553,6 +558,7 @@ def __init__(self, client: AsyncAgility) -> None: self.knowledge_bases = resources.AsyncKnowledgeBasesResourceWithRawResponse(client.knowledge_bases) self.users = resources.AsyncUsersResourceWithRawResponse(client.users) self.threads = resources.AsyncThreadsResourceWithRawResponse(client.threads) + self.integrations = resources.AsyncIntegrationsResourceWithRawResponse(client.integrations) class AgilityWithStreamedResponse: @@ -561,6 +567,7 @@ def __init__(self, client: Agility) -> None: self.knowledge_bases = resources.KnowledgeBasesResourceWithStreamingResponse(client.knowledge_bases) self.users = resources.UsersResourceWithStreamingResponse(client.users) self.threads = resources.ThreadsResourceWithStreamingResponse(client.threads) + self.integrations = resources.IntegrationsResourceWithStreamingResponse(client.integrations) class AsyncAgilityWithStreamedResponse: @@ -569,6 +576,7 @@ def __init__(self, client: AsyncAgility) -> None: self.knowledge_bases = resources.AsyncKnowledgeBasesResourceWithStreamingResponse(client.knowledge_bases) self.users = resources.AsyncUsersResourceWithStreamingResponse(client.users) self.threads = resources.AsyncThreadsResourceWithStreamingResponse(client.threads) + self.integrations = resources.AsyncIntegrationsResourceWithStreamingResponse(client.integrations) Client = Agility diff --git a/src/agility/resources/__init__.py b/src/agility/resources/__init__.py index ae9ca9a..7b8c074 100644 --- a/src/agility/resources/__init__.py +++ b/src/agility/resources/__init__.py @@ -24,6 +24,14 @@ AssistantsResourceWithStreamingResponse, AsyncAssistantsResourceWithStreamingResponse, ) +from .integrations import ( + IntegrationsResource, + AsyncIntegrationsResource, + IntegrationsResourceWithRawResponse, + AsyncIntegrationsResourceWithRawResponse, + IntegrationsResourceWithStreamingResponse, + AsyncIntegrationsResourceWithStreamingResponse, +) from .knowledge_bases import ( KnowledgeBasesResource, AsyncKnowledgeBasesResource, @@ -58,4 +66,10 @@ "AsyncThreadsResourceWithRawResponse", "ThreadsResourceWithStreamingResponse", "AsyncThreadsResourceWithStreamingResponse", + "IntegrationsResource", + "AsyncIntegrationsResource", + "IntegrationsResourceWithRawResponse", + "AsyncIntegrationsResourceWithRawResponse", + "IntegrationsResourceWithStreamingResponse", + "AsyncIntegrationsResourceWithStreamingResponse", ] diff --git a/src/agility/resources/integrations/__init__.py b/src/agility/resources/integrations/__init__.py new file mode 100644 index 0000000..2388422 --- /dev/null +++ b/src/agility/resources/integrations/__init__.py @@ -0,0 +1,47 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .rbac import ( + RbacResource, + AsyncRbacResource, + RbacResourceWithRawResponse, + AsyncRbacResourceWithRawResponse, + RbacResourceWithStreamingResponse, + AsyncRbacResourceWithStreamingResponse, +) +from .available import ( + AvailableResource, + AsyncAvailableResource, + AvailableResourceWithRawResponse, + AsyncAvailableResourceWithRawResponse, + AvailableResourceWithStreamingResponse, + AsyncAvailableResourceWithStreamingResponse, +) +from .integrations import ( + IntegrationsResource, + AsyncIntegrationsResource, + IntegrationsResourceWithRawResponse, + AsyncIntegrationsResourceWithRawResponse, + IntegrationsResourceWithStreamingResponse, + AsyncIntegrationsResourceWithStreamingResponse, +) + +__all__ = [ + "AvailableResource", + "AsyncAvailableResource", + "AvailableResourceWithRawResponse", + "AsyncAvailableResourceWithRawResponse", + "AvailableResourceWithStreamingResponse", + "AsyncAvailableResourceWithStreamingResponse", + "RbacResource", + "AsyncRbacResource", + "RbacResourceWithRawResponse", + "AsyncRbacResourceWithRawResponse", + "RbacResourceWithStreamingResponse", + "AsyncRbacResourceWithStreamingResponse", + "IntegrationsResource", + "AsyncIntegrationsResource", + "IntegrationsResourceWithRawResponse", + "AsyncIntegrationsResourceWithRawResponse", + "IntegrationsResourceWithStreamingResponse", + "AsyncIntegrationsResourceWithStreamingResponse", +] diff --git a/src/agility/resources/integrations/available.py b/src/agility/resources/integrations/available.py new file mode 100644 index 0000000..0f77dea --- /dev/null +++ b/src/agility/resources/integrations/available.py @@ -0,0 +1,135 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..._base_client import make_request_options +from ...types.integrations.available_list_response import AvailableListResponse + +__all__ = ["AvailableResource", "AsyncAvailableResource"] + + +class AvailableResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> AvailableResourceWithRawResponse: + """ + 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers + """ + return AvailableResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AvailableResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/agility-python#with_streaming_response + """ + return AvailableResourceWithStreamingResponse(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, + ) -> AvailableListResponse: + """Lists available integrations.""" + return self._get( + "/api/integrations/available", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AvailableListResponse, + ) + + +class AsyncAvailableResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncAvailableResourceWithRawResponse: + """ + 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers + """ + return AsyncAvailableResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncAvailableResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/agility-python#with_streaming_response + """ + return AsyncAvailableResourceWithStreamingResponse(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, + ) -> AvailableListResponse: + """Lists available integrations.""" + return await self._get( + "/api/integrations/available", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AvailableListResponse, + ) + + +class AvailableResourceWithRawResponse: + def __init__(self, available: AvailableResource) -> None: + self._available = available + + self.list = to_raw_response_wrapper( + available.list, + ) + + +class AsyncAvailableResourceWithRawResponse: + def __init__(self, available: AsyncAvailableResource) -> None: + self._available = available + + self.list = async_to_raw_response_wrapper( + available.list, + ) + + +class AvailableResourceWithStreamingResponse: + def __init__(self, available: AvailableResource) -> None: + self._available = available + + self.list = to_streamed_response_wrapper( + available.list, + ) + + +class AsyncAvailableResourceWithStreamingResponse: + def __init__(self, available: AsyncAvailableResource) -> None: + self._available = available + + self.list = async_to_streamed_response_wrapper( + available.list, + ) diff --git a/src/agility/resources/integrations/integrations.py b/src/agility/resources/integrations/integrations.py new file mode 100644 index 0000000..29d0539 --- /dev/null +++ b/src/agility/resources/integrations/integrations.py @@ -0,0 +1,519 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Any, cast + +import httpx + +from .rbac import ( + RbacResource, + AsyncRbacResource, + RbacResourceWithRawResponse, + AsyncRbacResourceWithRawResponse, + RbacResourceWithStreamingResponse, + AsyncRbacResourceWithStreamingResponse, +) +from ...types import integration_list_params, integration_create_params +from ..._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven +from ..._utils import ( + maybe_transform, + async_maybe_transform, +) +from ..._compat import cached_property +from .available import ( + AvailableResource, + AsyncAvailableResource, + AvailableResourceWithRawResponse, + AsyncAvailableResourceWithRawResponse, + AvailableResourceWithStreamingResponse, + AsyncAvailableResourceWithStreamingResponse, +) +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 SyncMyOffsetPage, AsyncMyOffsetPage +from ..._base_client import AsyncPaginator, make_request_options +from ...types.integration_list_response import IntegrationListResponse +from ...types.integration_create_response import IntegrationCreateResponse +from ...types.integration_retrieve_response import IntegrationRetrieveResponse + +__all__ = ["IntegrationsResource", "AsyncIntegrationsResource"] + + +class IntegrationsResource(SyncAPIResource): + @cached_property + def available(self) -> AvailableResource: + return AvailableResource(self._client) + + @cached_property + def rbac(self) -> RbacResource: + return RbacResource(self._client) + + @cached_property + def with_raw_response(self) -> IntegrationsResourceWithRawResponse: + """ + 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers + """ + return IntegrationsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> IntegrationsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/agility-python#with_streaming_response + """ + return IntegrationsResourceWithStreamingResponse(self) + + def create( + self, + *, + integration_params: integration_create_params.IntegrationParams, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> IntegrationCreateResponse: + """ + Creates a new integration. + + Args: + integration_params: S3 integration params model. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters 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 cast( + IntegrationCreateResponse, + self._post( + "/api/integrations/", + body=maybe_transform( + {"integration_params": integration_params}, integration_create_params.IntegrationCreateParams + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=cast( + Any, IntegrationCreateResponse + ), # Union types cannot be passed in as arguments in the type system + ), + ) + + def retrieve( + self, + integration_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> IntegrationRetrieveResponse: + """ + Gets an integration. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not integration_id: + raise ValueError(f"Expected a non-empty value for `integration_id` but received {integration_id!r}") + return cast( + IntegrationRetrieveResponse, + self._get( + f"/api/integrations/{integration_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=cast( + Any, IntegrationRetrieveResponse + ), # Union types cannot be passed in as arguments in the type system + ), + ) + + def list( + self, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncMyOffsetPage[IntegrationListResponse]: + """ + Lists integrations for a given knowledge base. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters 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( + "/api/integrations/", + page=SyncMyOffsetPage[IntegrationListResponse], + 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, + }, + integration_list_params.IntegrationListParams, + ), + ), + model=cast(Any, IntegrationListResponse), # Union types cannot be passed in as arguments in the type system + ) + + def delete( + self, + integration_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """ + Deletes an integration. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not integration_id: + raise ValueError(f"Expected a non-empty value for `integration_id` but received {integration_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + f"/api/integrations/{integration_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class AsyncIntegrationsResource(AsyncAPIResource): + @cached_property + def available(self) -> AsyncAvailableResource: + return AsyncAvailableResource(self._client) + + @cached_property + def rbac(self) -> AsyncRbacResource: + return AsyncRbacResource(self._client) + + @cached_property + def with_raw_response(self) -> AsyncIntegrationsResourceWithRawResponse: + """ + 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers + """ + return AsyncIntegrationsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncIntegrationsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/agility-python#with_streaming_response + """ + return AsyncIntegrationsResourceWithStreamingResponse(self) + + async def create( + self, + *, + integration_params: integration_create_params.IntegrationParams, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> IntegrationCreateResponse: + """ + Creates a new integration. + + Args: + integration_params: S3 integration params model. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters 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 cast( + IntegrationCreateResponse, + await self._post( + "/api/integrations/", + body=await async_maybe_transform( + {"integration_params": integration_params}, integration_create_params.IntegrationCreateParams + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=cast( + Any, IntegrationCreateResponse + ), # Union types cannot be passed in as arguments in the type system + ), + ) + + async def retrieve( + self, + integration_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> IntegrationRetrieveResponse: + """ + Gets an integration. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not integration_id: + raise ValueError(f"Expected a non-empty value for `integration_id` but received {integration_id!r}") + return cast( + IntegrationRetrieveResponse, + await self._get( + f"/api/integrations/{integration_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=cast( + Any, IntegrationRetrieveResponse + ), # Union types cannot be passed in as arguments in the type system + ), + ) + + def list( + self, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[IntegrationListResponse, AsyncMyOffsetPage[IntegrationListResponse]]: + """ + Lists integrations for a given knowledge base. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters 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( + "/api/integrations/", + page=AsyncMyOffsetPage[IntegrationListResponse], + 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, + }, + integration_list_params.IntegrationListParams, + ), + ), + model=cast(Any, IntegrationListResponse), # Union types cannot be passed in as arguments in the type system + ) + + async def delete( + self, + integration_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """ + Deletes an integration. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not integration_id: + raise ValueError(f"Expected a non-empty value for `integration_id` but received {integration_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + f"/api/integrations/{integration_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class IntegrationsResourceWithRawResponse: + def __init__(self, integrations: IntegrationsResource) -> None: + self._integrations = integrations + + self.create = to_raw_response_wrapper( + integrations.create, + ) + self.retrieve = to_raw_response_wrapper( + integrations.retrieve, + ) + self.list = to_raw_response_wrapper( + integrations.list, + ) + self.delete = to_raw_response_wrapper( + integrations.delete, + ) + + @cached_property + def available(self) -> AvailableResourceWithRawResponse: + return AvailableResourceWithRawResponse(self._integrations.available) + + @cached_property + def rbac(self) -> RbacResourceWithRawResponse: + return RbacResourceWithRawResponse(self._integrations.rbac) + + +class AsyncIntegrationsResourceWithRawResponse: + def __init__(self, integrations: AsyncIntegrationsResource) -> None: + self._integrations = integrations + + self.create = async_to_raw_response_wrapper( + integrations.create, + ) + self.retrieve = async_to_raw_response_wrapper( + integrations.retrieve, + ) + self.list = async_to_raw_response_wrapper( + integrations.list, + ) + self.delete = async_to_raw_response_wrapper( + integrations.delete, + ) + + @cached_property + def available(self) -> AsyncAvailableResourceWithRawResponse: + return AsyncAvailableResourceWithRawResponse(self._integrations.available) + + @cached_property + def rbac(self) -> AsyncRbacResourceWithRawResponse: + return AsyncRbacResourceWithRawResponse(self._integrations.rbac) + + +class IntegrationsResourceWithStreamingResponse: + def __init__(self, integrations: IntegrationsResource) -> None: + self._integrations = integrations + + self.create = to_streamed_response_wrapper( + integrations.create, + ) + self.retrieve = to_streamed_response_wrapper( + integrations.retrieve, + ) + self.list = to_streamed_response_wrapper( + integrations.list, + ) + self.delete = to_streamed_response_wrapper( + integrations.delete, + ) + + @cached_property + def available(self) -> AvailableResourceWithStreamingResponse: + return AvailableResourceWithStreamingResponse(self._integrations.available) + + @cached_property + def rbac(self) -> RbacResourceWithStreamingResponse: + return RbacResourceWithStreamingResponse(self._integrations.rbac) + + +class AsyncIntegrationsResourceWithStreamingResponse: + def __init__(self, integrations: AsyncIntegrationsResource) -> None: + self._integrations = integrations + + self.create = async_to_streamed_response_wrapper( + integrations.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + integrations.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + integrations.list, + ) + self.delete = async_to_streamed_response_wrapper( + integrations.delete, + ) + + @cached_property + def available(self) -> AsyncAvailableResourceWithStreamingResponse: + return AsyncAvailableResourceWithStreamingResponse(self._integrations.available) + + @cached_property + def rbac(self) -> AsyncRbacResourceWithStreamingResponse: + return AsyncRbacResourceWithStreamingResponse(self._integrations.rbac) diff --git a/src/agility/resources/integrations/rbac.py b/src/agility/resources/integrations/rbac.py new file mode 100644 index 0000000..a758cde --- /dev/null +++ b/src/agility/resources/integrations/rbac.py @@ -0,0 +1,163 @@ +# 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.integration import Integration + +__all__ = ["RbacResource", "AsyncRbacResource"] + + +class RbacResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> RbacResourceWithRawResponse: + """ + 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers + """ + return RbacResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> RbacResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/agility-python#with_streaming_response + """ + return RbacResourceWithStreamingResponse(self) + + def verify( + self, + integration_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Integration: + """ + Verifies an integration. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not integration_id: + raise ValueError(f"Expected a non-empty value for `integration_id` but received {integration_id!r}") + return self._post( + f"/api/integrations/rbac/{integration_id}/verify", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Integration, + ) + + +class AsyncRbacResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncRbacResourceWithRawResponse: + """ + 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers + """ + return AsyncRbacResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncRbacResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/agility-python#with_streaming_response + """ + return AsyncRbacResourceWithStreamingResponse(self) + + async def verify( + self, + integration_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Integration: + """ + Verifies an integration. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not integration_id: + raise ValueError(f"Expected a non-empty value for `integration_id` but received {integration_id!r}") + return await self._post( + f"/api/integrations/rbac/{integration_id}/verify", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Integration, + ) + + +class RbacResourceWithRawResponse: + def __init__(self, rbac: RbacResource) -> None: + self._rbac = rbac + + self.verify = to_raw_response_wrapper( + rbac.verify, + ) + + +class AsyncRbacResourceWithRawResponse: + def __init__(self, rbac: AsyncRbacResource) -> None: + self._rbac = rbac + + self.verify = async_to_raw_response_wrapper( + rbac.verify, + ) + + +class RbacResourceWithStreamingResponse: + def __init__(self, rbac: RbacResource) -> None: + self._rbac = rbac + + self.verify = to_streamed_response_wrapper( + rbac.verify, + ) + + +class AsyncRbacResourceWithStreamingResponse: + def __init__(self, rbac: AsyncRbacResource) -> None: + self._rbac = rbac + + self.verify = async_to_streamed_response_wrapper( + rbac.verify, + ) diff --git a/src/agility/types/__init__.py b/src/agility/types/__init__.py index c93640a..ef19224 100644 --- a/src/agility/types/__init__.py +++ b/src/agility/types/__init__.py @@ -5,12 +5,20 @@ from .user import User as User from .thread import Thread as Thread from .assistant import Assistant as Assistant +from .integration import Integration as Integration +from .s3_v0_integration import S3V0Integration as S3V0Integration +from .gc_sv0_integration import GcSv0Integration as GcSv0Integration from .thread_list_params import ThreadListParams as ThreadListParams from .assistant_list_params import AssistantListParams as AssistantListParams from .assistant_with_config import AssistantWithConfig as AssistantWithConfig from .assistant_create_params import AssistantCreateParams as AssistantCreateParams from .assistant_update_params import AssistantUpdateParams as AssistantUpdateParams +from .integration_list_params import IntegrationListParams as IntegrationListParams +from .integration_create_params import IntegrationCreateParams as IntegrationCreateParams +from .integration_list_response import IntegrationListResponse as IntegrationListResponse from .knowledge_base_list_params import KnowledgeBaseListParams as KnowledgeBaseListParams from .knowledge_base_with_config import KnowledgeBaseWithConfig as KnowledgeBaseWithConfig +from .integration_create_response import IntegrationCreateResponse as IntegrationCreateResponse from .knowledge_base_create_params import KnowledgeBaseCreateParams as KnowledgeBaseCreateParams from .knowledge_base_update_params import KnowledgeBaseUpdateParams as KnowledgeBaseUpdateParams +from .integration_retrieve_response import IntegrationRetrieveResponse as IntegrationRetrieveResponse diff --git a/src/agility/types/gc_sv0_integration.py b/src/agility/types/gc_sv0_integration.py new file mode 100644 index 0000000..71017f3 --- /dev/null +++ b/src/agility/types/gc_sv0_integration.py @@ -0,0 +1,30 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["GcSv0Integration", "ResourceAccessDefinition", "ResourceAccessDefinitionResource"] + + +class ResourceAccessDefinitionResource(BaseModel): + resource_type: Optional[Literal["gcs/v0"]] = None + + +class ResourceAccessDefinition(BaseModel): + resource: ResourceAccessDefinitionResource + + +class GcSv0Integration(BaseModel): + id: str + + principal_id: str + + resource_access_definition: ResourceAccessDefinition + + state: Literal["ready", "pending", "error"] + + integration_category: Optional[Literal["rbac"]] = None + + integration_type: Optional[Literal["s3/v0", "gcs/v0"]] = None diff --git a/src/agility/types/integration.py b/src/agility/types/integration.py new file mode 100644 index 0000000..8f2c32b --- /dev/null +++ b/src/agility/types/integration.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["Integration"] + + +class Integration(BaseModel): + id: str + + integration_category: Literal["rbac"] + + integration_type: Literal["s3/v0", "gcs/v0"] + + state: Literal["ready", "pending", "error"] diff --git a/src/agility/types/integration_create_params.py b/src/agility/types/integration_create_params.py new file mode 100644 index 0000000..f774b02 --- /dev/null +++ b/src/agility/types/integration_create_params.py @@ -0,0 +1,51 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union +from typing_extensions import Literal, Required, TypeAlias, TypedDict + +__all__ = [ + "IntegrationCreateParams", + "IntegrationParams", + "IntegrationParamsS3IntegrationParamsV0", + "IntegrationParamsS3IntegrationParamsV0Resource", + "IntegrationParamsGcsIntegrationParamsV0", + "IntegrationParamsGcsIntegrationParamsV0Resource", +] + + +class IntegrationCreateParams(TypedDict, total=False): + integration_params: Required[IntegrationParams] + """S3 integration params model.""" + + +class IntegrationParamsS3IntegrationParamsV0Resource(TypedDict, total=False): + bucket_name: Required[str] + + prefix: Required[str] + + resource_type: Literal["s3/v0"] + + +class IntegrationParamsS3IntegrationParamsV0(TypedDict, total=False): + resource: Required[IntegrationParamsS3IntegrationParamsV0Resource] + + integration_category: Literal["rbac"] + + integration_type: Literal["s3/v0"] + + +class IntegrationParamsGcsIntegrationParamsV0Resource(TypedDict, total=False): + resource_type: Literal["gcs/v0"] + + +class IntegrationParamsGcsIntegrationParamsV0(TypedDict, total=False): + resource: Required[IntegrationParamsGcsIntegrationParamsV0Resource] + + integration_category: Literal["rbac"] + + integration_type: Literal["gcs/v0"] + + +IntegrationParams: TypeAlias = Union[IntegrationParamsS3IntegrationParamsV0, IntegrationParamsGcsIntegrationParamsV0] diff --git a/src/agility/types/integration_create_response.py b/src/agility/types/integration_create_response.py new file mode 100644 index 0000000..3c15227 --- /dev/null +++ b/src/agility/types/integration_create_response.py @@ -0,0 +1,11 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Union +from typing_extensions import TypeAlias + +from .s3_v0_integration import S3V0Integration +from .gc_sv0_integration import GcSv0Integration + +__all__ = ["IntegrationCreateResponse"] + +IntegrationCreateResponse: TypeAlias = Union[S3V0Integration, GcSv0Integration] diff --git a/src/agility/types/integration_list_params.py b/src/agility/types/integration_list_params.py new file mode 100644 index 0000000..43a5171 --- /dev/null +++ b/src/agility/types/integration_list_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 TypedDict + +__all__ = ["IntegrationListParams"] + + +class IntegrationListParams(TypedDict, total=False): + limit: int + + offset: int diff --git a/src/agility/types/integration_list_response.py b/src/agility/types/integration_list_response.py new file mode 100644 index 0000000..a8b8b79 --- /dev/null +++ b/src/agility/types/integration_list_response.py @@ -0,0 +1,11 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Union +from typing_extensions import TypeAlias + +from .s3_v0_integration import S3V0Integration +from .gc_sv0_integration import GcSv0Integration + +__all__ = ["IntegrationListResponse"] + +IntegrationListResponse: TypeAlias = Union[S3V0Integration, GcSv0Integration] diff --git a/src/agility/types/integration_retrieve_response.py b/src/agility/types/integration_retrieve_response.py new file mode 100644 index 0000000..f634a6f --- /dev/null +++ b/src/agility/types/integration_retrieve_response.py @@ -0,0 +1,11 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Union +from typing_extensions import TypeAlias + +from .s3_v0_integration import S3V0Integration +from .gc_sv0_integration import GcSv0Integration + +__all__ = ["IntegrationRetrieveResponse"] + +IntegrationRetrieveResponse: TypeAlias = Union[S3V0Integration, GcSv0Integration] diff --git a/src/agility/types/integrations/__init__.py b/src/agility/types/integrations/__init__.py new file mode 100644 index 0000000..dae3001 --- /dev/null +++ b/src/agility/types/integrations/__init__.py @@ -0,0 +1,6 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .integration_type_def import IntegrationTypeDef as IntegrationTypeDef +from .available_list_response import AvailableListResponse as AvailableListResponse diff --git a/src/agility/types/integrations/available_list_response.py b/src/agility/types/integrations/available_list_response.py new file mode 100644 index 0000000..466b38e --- /dev/null +++ b/src/agility/types/integrations/available_list_response.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List +from typing_extensions import TypeAlias + +from .integration_type_def import IntegrationTypeDef + +__all__ = ["AvailableListResponse"] + +AvailableListResponse: TypeAlias = List[IntegrationTypeDef] diff --git a/src/agility/types/integrations/integration_type_def.py b/src/agility/types/integrations/integration_type_def.py new file mode 100644 index 0000000..01b9d48 --- /dev/null +++ b/src/agility/types/integrations/integration_type_def.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["IntegrationTypeDef"] + + +class IntegrationTypeDef(BaseModel): + description: str + + integration_category: Literal["rbac"] + + integration_type: Literal["s3/v0", "gcs/v0"] + + name: str diff --git a/src/agility/types/s3_v0_integration.py b/src/agility/types/s3_v0_integration.py new file mode 100644 index 0000000..ee0751a --- /dev/null +++ b/src/agility/types/s3_v0_integration.py @@ -0,0 +1,36 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["S3V0Integration", "ResourceAccessDefinition", "ResourceAccessDefinitionResource"] + + +class ResourceAccessDefinitionResource(BaseModel): + bucket_name: str + + prefix: str + + resource_type: Optional[Literal["s3/v0"]] = None + + +class ResourceAccessDefinition(BaseModel): + policy: object + + resource: ResourceAccessDefinitionResource + + +class S3V0Integration(BaseModel): + id: str + + principal_id: str + + resource_access_definition: ResourceAccessDefinition + + state: Literal["ready", "pending", "error"] + + integration_category: Optional[Literal["rbac"]] = None + + integration_type: Optional[Literal["s3/v0", "gcs/v0"]] = None diff --git a/tests/api_resources/integrations/__init__.py b/tests/api_resources/integrations/__init__.py new file mode 100644 index 0000000..fd8019a --- /dev/null +++ b/tests/api_resources/integrations/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/integrations/test_available.py b/tests/api_resources/integrations/test_available.py new file mode 100644 index 0000000..36a89f0 --- /dev/null +++ b/tests/api_resources/integrations/test_available.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 agility import Agility, AsyncAgility +from tests.utils import assert_matches_type +from agility.types.integrations import AvailableListResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestAvailable: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_list(self, client: Agility) -> None: + available = client.integrations.available.list() + assert_matches_type(AvailableListResponse, available, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Agility) -> None: + response = client.integrations.available.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + available = response.parse() + assert_matches_type(AvailableListResponse, available, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Agility) -> None: + with client.integrations.available.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + available = response.parse() + assert_matches_type(AvailableListResponse, available, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncAvailable: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_list(self, async_client: AsyncAgility) -> None: + available = await async_client.integrations.available.list() + assert_matches_type(AvailableListResponse, available, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncAgility) -> None: + response = await async_client.integrations.available.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + available = await response.parse() + assert_matches_type(AvailableListResponse, available, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncAgility) -> None: + async with async_client.integrations.available.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + available = await response.parse() + assert_matches_type(AvailableListResponse, available, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/integrations/test_rbac.py b/tests/api_resources/integrations/test_rbac.py new file mode 100644 index 0000000..bb00ede --- /dev/null +++ b/tests/api_resources/integrations/test_rbac.py @@ -0,0 +1,98 @@ +# 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 agility import Agility, AsyncAgility +from tests.utils import assert_matches_type +from agility.types import Integration + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestRbac: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_verify(self, client: Agility) -> None: + rbac = client.integrations.rbac.verify( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(Integration, rbac, path=["response"]) + + @parametrize + def test_raw_response_verify(self, client: Agility) -> None: + response = client.integrations.rbac.with_raw_response.verify( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + rbac = response.parse() + assert_matches_type(Integration, rbac, path=["response"]) + + @parametrize + def test_streaming_response_verify(self, client: Agility) -> None: + with client.integrations.rbac.with_streaming_response.verify( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + rbac = response.parse() + assert_matches_type(Integration, rbac, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_verify(self, client: Agility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `integration_id` but received ''"): + client.integrations.rbac.with_raw_response.verify( + "", + ) + + +class TestAsyncRbac: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_verify(self, async_client: AsyncAgility) -> None: + rbac = await async_client.integrations.rbac.verify( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(Integration, rbac, path=["response"]) + + @parametrize + async def test_raw_response_verify(self, async_client: AsyncAgility) -> None: + response = await async_client.integrations.rbac.with_raw_response.verify( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + rbac = await response.parse() + assert_matches_type(Integration, rbac, path=["response"]) + + @parametrize + async def test_streaming_response_verify(self, async_client: AsyncAgility) -> None: + async with async_client.integrations.rbac.with_streaming_response.verify( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + rbac = await response.parse() + assert_matches_type(Integration, rbac, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_verify(self, async_client: AsyncAgility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `integration_id` but received ''"): + await async_client.integrations.rbac.with_raw_response.verify( + "", + ) diff --git a/tests/api_resources/test_integrations.py b/tests/api_resources/test_integrations.py new file mode 100644 index 0000000..5b92346 --- /dev/null +++ b/tests/api_resources/test_integrations.py @@ -0,0 +1,367 @@ +# 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 agility import Agility, AsyncAgility +from tests.utils import assert_matches_type +from agility.types import ( + IntegrationListResponse, + IntegrationCreateResponse, + IntegrationRetrieveResponse, +) +from agility.pagination import SyncMyOffsetPage, AsyncMyOffsetPage + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestIntegrations: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: Agility) -> None: + integration = client.integrations.create( + integration_params={ + "resource": { + "bucket_name": "bucket_name", + "prefix": "prefix", + } + }, + ) + assert_matches_type(IntegrationCreateResponse, integration, path=["response"]) + + @parametrize + def test_method_create_with_all_params(self, client: Agility) -> None: + integration = client.integrations.create( + integration_params={ + "resource": { + "bucket_name": "bucket_name", + "prefix": "prefix", + "resource_type": "s3/v0", + }, + "integration_category": "rbac", + "integration_type": "s3/v0", + }, + ) + assert_matches_type(IntegrationCreateResponse, integration, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Agility) -> None: + response = client.integrations.with_raw_response.create( + integration_params={ + "resource": { + "bucket_name": "bucket_name", + "prefix": "prefix", + } + }, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + integration = response.parse() + assert_matches_type(IntegrationCreateResponse, integration, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Agility) -> None: + with client.integrations.with_streaming_response.create( + integration_params={ + "resource": { + "bucket_name": "bucket_name", + "prefix": "prefix", + } + }, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + integration = response.parse() + assert_matches_type(IntegrationCreateResponse, integration, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_retrieve(self, client: Agility) -> None: + integration = client.integrations.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(IntegrationRetrieveResponse, integration, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Agility) -> None: + response = client.integrations.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" + integration = response.parse() + assert_matches_type(IntegrationRetrieveResponse, integration, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Agility) -> None: + with client.integrations.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" + + integration = response.parse() + assert_matches_type(IntegrationRetrieveResponse, integration, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Agility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `integration_id` but received ''"): + client.integrations.with_raw_response.retrieve( + "", + ) + + @parametrize + def test_method_list(self, client: Agility) -> None: + integration = client.integrations.list() + assert_matches_type(SyncMyOffsetPage[IntegrationListResponse], integration, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Agility) -> None: + integration = client.integrations.list( + limit=1, + offset=0, + ) + assert_matches_type(SyncMyOffsetPage[IntegrationListResponse], integration, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Agility) -> None: + response = client.integrations.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + integration = response.parse() + assert_matches_type(SyncMyOffsetPage[IntegrationListResponse], integration, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Agility) -> None: + with client.integrations.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + integration = response.parse() + assert_matches_type(SyncMyOffsetPage[IntegrationListResponse], integration, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_delete(self, client: Agility) -> None: + integration = client.integrations.delete( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert integration is None + + @parametrize + def test_raw_response_delete(self, client: Agility) -> None: + response = client.integrations.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" + integration = response.parse() + assert integration is None + + @parametrize + def test_streaming_response_delete(self, client: Agility) -> None: + with client.integrations.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" + + integration = response.parse() + assert integration is None + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_delete(self, client: Agility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `integration_id` but received ''"): + client.integrations.with_raw_response.delete( + "", + ) + + +class TestAsyncIntegrations: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_create(self, async_client: AsyncAgility) -> None: + integration = await async_client.integrations.create( + integration_params={ + "resource": { + "bucket_name": "bucket_name", + "prefix": "prefix", + } + }, + ) + assert_matches_type(IntegrationCreateResponse, integration, path=["response"]) + + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncAgility) -> None: + integration = await async_client.integrations.create( + integration_params={ + "resource": { + "bucket_name": "bucket_name", + "prefix": "prefix", + "resource_type": "s3/v0", + }, + "integration_category": "rbac", + "integration_type": "s3/v0", + }, + ) + assert_matches_type(IntegrationCreateResponse, integration, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncAgility) -> None: + response = await async_client.integrations.with_raw_response.create( + integration_params={ + "resource": { + "bucket_name": "bucket_name", + "prefix": "prefix", + } + }, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + integration = await response.parse() + assert_matches_type(IntegrationCreateResponse, integration, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncAgility) -> None: + async with async_client.integrations.with_streaming_response.create( + integration_params={ + "resource": { + "bucket_name": "bucket_name", + "prefix": "prefix", + } + }, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + integration = await response.parse() + assert_matches_type(IntegrationCreateResponse, integration, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_retrieve(self, async_client: AsyncAgility) -> None: + integration = await async_client.integrations.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(IntegrationRetrieveResponse, integration, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncAgility) -> None: + response = await async_client.integrations.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" + integration = await response.parse() + assert_matches_type(IntegrationRetrieveResponse, integration, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncAgility) -> None: + async with async_client.integrations.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" + + integration = await response.parse() + assert_matches_type(IntegrationRetrieveResponse, integration, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncAgility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `integration_id` but received ''"): + await async_client.integrations.with_raw_response.retrieve( + "", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncAgility) -> None: + integration = await async_client.integrations.list() + assert_matches_type(AsyncMyOffsetPage[IntegrationListResponse], integration, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncAgility) -> None: + integration = await async_client.integrations.list( + limit=1, + offset=0, + ) + assert_matches_type(AsyncMyOffsetPage[IntegrationListResponse], integration, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncAgility) -> None: + response = await async_client.integrations.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + integration = await response.parse() + assert_matches_type(AsyncMyOffsetPage[IntegrationListResponse], integration, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncAgility) -> None: + async with async_client.integrations.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + integration = await response.parse() + assert_matches_type(AsyncMyOffsetPage[IntegrationListResponse], integration, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_delete(self, async_client: AsyncAgility) -> None: + integration = await async_client.integrations.delete( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert integration is None + + @parametrize + async def test_raw_response_delete(self, async_client: AsyncAgility) -> None: + response = await async_client.integrations.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" + integration = await response.parse() + assert integration is None + + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncAgility) -> None: + async with async_client.integrations.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" + + integration = await response.parse() + assert integration is None + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_delete(self, async_client: AsyncAgility) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `integration_id` but received ''"): + await async_client.integrations.with_raw_response.delete( + "", + ) From 352e613453bfa46e1ec8c6f0b176a45c3c464388 Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Wed, 6 Nov 2024 19:08:03 +0000 Subject: [PATCH 12/77] chore: rebuild project due to codegen change --- tests/test_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 82eaed7..1d75133 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -689,7 +689,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)) @@ -1486,7 +1486,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 7b9b6d2a57b50b036aa2aa9ff3b7368dfe403543 Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Tue, 12 Nov 2024 12:13:09 +0000 Subject: [PATCH 13/77] chore: rebuild project due to codegen change --- src/agility/_utils/_transform.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/agility/_utils/_transform.py b/src/agility/_utils/_transform.py index d7c0534..a6b62ca 100644 --- a/src/agility/_utils/_transform.py +++ b/src/agility/_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] From 50ed388aeaaa19dc836f84c8e6b180bbc9b64ca5 Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Fri, 15 Nov 2024 20:17:35 +0000 Subject: [PATCH 14/77] feat(api): api update --- .stats.yml | 2 +- src/agility/types/knowledge_bases/source.py | 12 ++++++++++++ .../types/knowledge_bases/source_create_params.py | 12 ++++++++++++ .../types/knowledge_bases/source_update_params.py | 12 ++++++++++++ tests/api_resources/knowledge_bases/test_sources.py | 4 ++++ 5 files changed, 41 insertions(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 20ebe85..ae55eea 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 42 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-4abd340ba528b5912ad6672a203de3c6904577d27b3397533d51d3ddc265339f.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-357b7ce8fa3860bfc660f714c48ef05944b5e963814c8e935dda9863cf8ed8aa.yml diff --git a/src/agility/types/knowledge_bases/source.py b/src/agility/types/knowledge_bases/source.py index dbb3326..028402f 100644 --- a/src/agility/types/knowledge_bases/source.py +++ b/src/agility/types/knowledge_bases/source.py @@ -11,6 +11,7 @@ "Source", "SourceParams", "SourceParamsWebV0Params", + "SourceParamsWebV0ParamsScrapeOptions", "SourceParamsNotionParams", "SourceParamsS3PublicV0Params", "SourceParamsS3PrivateV0Params", @@ -18,6 +19,14 @@ ] +class SourceParamsWebV0ParamsScrapeOptions(BaseModel): + wait_for: Optional[int] = None + """ + Amount of time (in milliseconds) to wait for each page to load before scraping + content. + """ + + class SourceParamsWebV0Params(BaseModel): urls: List[str] @@ -35,6 +44,9 @@ class SourceParamsWebV0Params(BaseModel): name: Optional[Literal["web_v0"]] = None + scrape_options: Optional[SourceParamsWebV0ParamsScrapeOptions] = None + """Parameters for scraping each crawled page.""" + class SourceParamsNotionParams(BaseModel): name: Optional[Literal["notion"]] = None diff --git a/src/agility/types/knowledge_bases/source_create_params.py b/src/agility/types/knowledge_bases/source_create_params.py index 03bc9a3..6450436 100644 --- a/src/agility/types/knowledge_bases/source_create_params.py +++ b/src/agility/types/knowledge_bases/source_create_params.py @@ -9,6 +9,7 @@ "SourceCreateParams", "SourceParams", "SourceParamsWebV0Params", + "SourceParamsWebV0ParamsScrapeOptions", "SourceParamsNotionParams", "SourceParamsS3PublicV0Params", "SourceParamsS3PrivateV0Params", @@ -30,6 +31,14 @@ class SourceCreateParams(TypedDict, total=False): sync: bool +class SourceParamsWebV0ParamsScrapeOptions(TypedDict, total=False): + wait_for: int + """ + Amount of time (in milliseconds) to wait for each page to load before scraping + content. + """ + + class SourceParamsWebV0Params(TypedDict, total=False): urls: Required[List[str]] @@ -47,6 +56,9 @@ class SourceParamsWebV0Params(TypedDict, total=False): name: Literal["web_v0"] + scrape_options: SourceParamsWebV0ParamsScrapeOptions + """Parameters for scraping each crawled page.""" + class SourceParamsNotionParams(TypedDict, total=False): name: Literal["notion"] diff --git a/src/agility/types/knowledge_bases/source_update_params.py b/src/agility/types/knowledge_bases/source_update_params.py index b2cabb2..7c29a51 100644 --- a/src/agility/types/knowledge_bases/source_update_params.py +++ b/src/agility/types/knowledge_bases/source_update_params.py @@ -9,6 +9,7 @@ "SourceUpdateParams", "SourceParams", "SourceParamsWebV0Params", + "SourceParamsWebV0ParamsScrapeOptions", "SourceParamsNotionParams", "SourceParamsS3PublicV0Params", "SourceParamsS3PrivateV0Params", @@ -32,6 +33,14 @@ class SourceUpdateParams(TypedDict, total=False): sync: bool +class SourceParamsWebV0ParamsScrapeOptions(TypedDict, total=False): + wait_for: int + """ + Amount of time (in milliseconds) to wait for each page to load before scraping + content. + """ + + class SourceParamsWebV0Params(TypedDict, total=False): urls: Required[List[str]] @@ -49,6 +58,9 @@ class SourceParamsWebV0Params(TypedDict, total=False): name: Literal["web_v0"] + scrape_options: SourceParamsWebV0ParamsScrapeOptions + """Parameters for scraping each crawled page.""" + class SourceParamsNotionParams(TypedDict, total=False): name: Literal["notion"] diff --git a/tests/api_resources/knowledge_bases/test_sources.py b/tests/api_resources/knowledge_bases/test_sources.py index 0446b1e..214706f 100644 --- a/tests/api_resources/knowledge_bases/test_sources.py +++ b/tests/api_resources/knowledge_bases/test_sources.py @@ -50,6 +50,7 @@ def test_method_create_with_all_params(self, client: Agility) -> None: "limit": 0, "max_depth": 0, "name": "web_v0", + "scrape_options": {"wait_for": 0}, }, source_schedule={ "cron": "cron", @@ -190,6 +191,7 @@ def test_method_update_with_all_params(self, client: Agility) -> None: "limit": 0, "max_depth": 0, "name": "web_v0", + "scrape_options": {"wait_for": 0}, }, source_schedule={ "cron": "cron", @@ -491,6 +493,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgility) - "limit": 0, "max_depth": 0, "name": "web_v0", + "scrape_options": {"wait_for": 0}, }, source_schedule={ "cron": "cron", @@ -631,6 +634,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncAgility) - "limit": 0, "max_depth": 0, "name": "web_v0", + "scrape_options": {"wait_for": 0}, }, source_schedule={ "cron": "cron", From 276697f92099b5296babdeeb3e7d6b748b868321 Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Mon, 18 Nov 2024 10:30:25 +0000 Subject: [PATCH 15/77] chore: rebuild project due to codegen change --- .../knowledge_bases/test_sources.py | 44 +++++++-------- tests/api_resources/test_assistants.py | 8 +-- tests/api_resources/threads/test_runs.py | 56 ++----------------- 3 files changed, 30 insertions(+), 78 deletions(-) diff --git a/tests/api_resources/knowledge_bases/test_sources.py b/tests/api_resources/knowledge_bases/test_sources.py index 214706f..7a854b9 100644 --- a/tests/api_resources/knowledge_bases/test_sources.py +++ b/tests/api_resources/knowledge_bases/test_sources.py @@ -27,7 +27,7 @@ def test_method_create(self, client: Agility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string", "string", "string"]}, + source_params={"urls": ["string"]}, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -42,7 +42,7 @@ def test_method_create_with_all_params(self, client: Agility) -> None: description="description", name="name", source_params={ - "urls": ["string", "string", "string"], + "urls": ["string"], "allow_backward_links": True, "allow_external_links": True, "exclude_regex": "exclude_regex", @@ -66,7 +66,7 @@ def test_raw_response_create(self, client: Agility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string", "string", "string"]}, + source_params={"urls": ["string"]}, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -84,7 +84,7 @@ def test_streaming_response_create(self, client: Agility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string", "string", "string"]}, + source_params={"urls": ["string"]}, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -105,7 +105,7 @@ def test_path_params_create(self, client: Agility) -> None: knowledge_base_id="", description="description", name="name", - source_params={"urls": ["string", "string", "string"]}, + source_params={"urls": ["string"]}, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -167,7 +167,7 @@ def test_method_update(self, client: Agility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string", "string", "string"]}, + source_params={"urls": ["string"]}, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -183,7 +183,7 @@ def test_method_update_with_all_params(self, client: Agility) -> None: description="description", name="name", source_params={ - "urls": ["string", "string", "string"], + "urls": ["string"], "allow_backward_links": True, "allow_external_links": True, "exclude_regex": "exclude_regex", @@ -208,7 +208,7 @@ def test_raw_response_update(self, client: Agility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string", "string", "string"]}, + source_params={"urls": ["string"]}, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -227,7 +227,7 @@ def test_streaming_response_update(self, client: Agility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string", "string", "string"]}, + source_params={"urls": ["string"]}, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -249,7 +249,7 @@ def test_path_params_update(self, client: Agility) -> None: knowledge_base_id="", description="description", name="name", - source_params={"urls": ["string", "string", "string"]}, + source_params={"urls": ["string"]}, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -262,7 +262,7 @@ def test_path_params_update(self, client: Agility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string", "string", "string"]}, + source_params={"urls": ["string"]}, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -470,7 +470,7 @@ async def test_method_create(self, async_client: AsyncAgility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string", "string", "string"]}, + source_params={"urls": ["string"]}, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -485,7 +485,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgility) - description="description", name="name", source_params={ - "urls": ["string", "string", "string"], + "urls": ["string"], "allow_backward_links": True, "allow_external_links": True, "exclude_regex": "exclude_regex", @@ -509,7 +509,7 @@ async def test_raw_response_create(self, async_client: AsyncAgility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string", "string", "string"]}, + source_params={"urls": ["string"]}, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -527,7 +527,7 @@ async def test_streaming_response_create(self, async_client: AsyncAgility) -> No knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string", "string", "string"]}, + source_params={"urls": ["string"]}, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -548,7 +548,7 @@ async def test_path_params_create(self, async_client: AsyncAgility) -> None: knowledge_base_id="", description="description", name="name", - source_params={"urls": ["string", "string", "string"]}, + source_params={"urls": ["string"]}, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -610,7 +610,7 @@ async def test_method_update(self, async_client: AsyncAgility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string", "string", "string"]}, + source_params={"urls": ["string"]}, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -626,7 +626,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncAgility) - description="description", name="name", source_params={ - "urls": ["string", "string", "string"], + "urls": ["string"], "allow_backward_links": True, "allow_external_links": True, "exclude_regex": "exclude_regex", @@ -651,7 +651,7 @@ async def test_raw_response_update(self, async_client: AsyncAgility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string", "string", "string"]}, + source_params={"urls": ["string"]}, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -670,7 +670,7 @@ async def test_streaming_response_update(self, async_client: AsyncAgility) -> No knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string", "string", "string"]}, + source_params={"urls": ["string"]}, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -692,7 +692,7 @@ async def test_path_params_update(self, async_client: AsyncAgility) -> None: knowledge_base_id="", description="description", name="name", - source_params={"urls": ["string", "string", "string"]}, + source_params={"urls": ["string"]}, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -705,7 +705,7 @@ async def test_path_params_update(self, async_client: AsyncAgility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string", "string", "string"]}, + source_params={"urls": ["string"]}, source_schedule={ "cron": "cron", "utc_offset": 0, diff --git a/tests/api_resources/test_assistants.py b/tests/api_resources/test_assistants.py index 27cdc31..d29c7ee 100644 --- a/tests/api_resources/test_assistants.py +++ b/tests/api_resources/test_assistants.py @@ -38,7 +38,7 @@ def test_method_create_with_all_params(self, client: Agility) -> None: name="name", instructions="instructions", model="gpt-4o", - suggested_questions=["string", "string", "string"], + suggested_questions=["string"], url_slug="url_slug", ) assert_matches_type(Assistant, assistant, path=["response"]) @@ -130,7 +130,7 @@ def test_method_update_with_all_params(self, client: Agility) -> None: name="name", instructions="instructions", model="gpt-4o", - suggested_questions=["string", "string", "string"], + suggested_questions=["string"], url_slug="url_slug", ) assert_matches_type(AssistantWithConfig, assistant, path=["response"]) @@ -270,7 +270,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgility) - name="name", instructions="instructions", model="gpt-4o", - suggested_questions=["string", "string", "string"], + suggested_questions=["string"], url_slug="url_slug", ) assert_matches_type(Assistant, assistant, path=["response"]) @@ -362,7 +362,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncAgility) - name="name", instructions="instructions", model="gpt-4o", - suggested_questions=["string", "string", "string"], + suggested_questions=["string"], url_slug="url_slug", ) assert_matches_type(AssistantWithConfig, assistant, path=["response"]) diff --git a/tests/api_resources/threads/test_runs.py b/tests/api_resources/threads/test_runs.py index d286533..55f611c 100644 --- a/tests/api_resources/threads/test_runs.py +++ b/tests/api_resources/threads/test_runs.py @@ -37,19 +37,7 @@ def test_method_create_with_all_params(self, client: Agility) -> None: "metadata": {"trustworthiness_score": 0}, "role": "user", "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - }, - { - "content": "content", - "metadata": {"trustworthiness_score": 0}, - "role": "user", - "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - }, - { - "content": "content", - "metadata": {"trustworthiness_score": 0}, - "role": "user", - "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - }, + } ], instructions="instructions", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", @@ -207,19 +195,7 @@ def test_method_stream_with_all_params(self, client: Agility) -> None: "metadata": {"trustworthiness_score": 0}, "role": "user", "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - }, - { - "content": "content", - "metadata": {"trustworthiness_score": 0}, - "role": "user", - "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - }, - { - "content": "content", - "metadata": {"trustworthiness_score": 0}, - "role": "user", - "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - }, + } ], instructions="instructions", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", @@ -285,19 +261,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgility) - "metadata": {"trustworthiness_score": 0}, "role": "user", "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - }, - { - "content": "content", - "metadata": {"trustworthiness_score": 0}, - "role": "user", - "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - }, - { - "content": "content", - "metadata": {"trustworthiness_score": 0}, - "role": "user", - "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - }, + } ], instructions="instructions", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", @@ -455,19 +419,7 @@ async def test_method_stream_with_all_params(self, async_client: AsyncAgility) - "metadata": {"trustworthiness_score": 0}, "role": "user", "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - }, - { - "content": "content", - "metadata": {"trustworthiness_score": 0}, - "role": "user", - "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - }, - { - "content": "content", - "metadata": {"trustworthiness_score": 0}, - "role": "user", - "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - }, + } ], instructions="instructions", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", From fa5935bfd1e6321052f674e03ef59d65f39b51e6 Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Mon, 18 Nov 2024 12:48:29 +0000 Subject: [PATCH 16/77] chore: rebuild project due to codegen change --- pyproject.toml | 1 + requirements-dev.lock | 1 + src/agility/_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 0714767..c4a0415 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 c626f3c..46ba92e 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/agility/_utils/_sync.py b/src/agility/_utils/_sync.py index d0d8103..8b3aaf2 100644 --- a/src/agility/_utils/_sync.py +++ b/src/agility/_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 1d75133..afe7f9f 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 @@ -1630,3 +1633,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 agility._utils import asyncify + from agility._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 03cbea20684cd6870e7c4aae1ac9935964b64bcc Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Wed, 20 Nov 2024 06:17:55 +0000 Subject: [PATCH 17/77] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index ae55eea..b6ba801 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 42 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-357b7ce8fa3860bfc660f714c48ef05944b5e963814c8e935dda9863cf8ed8aa.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-d630ad236884869cf0899ddac1e8b99e0854f5fec3878ce31b7a6403e0a2791c.yml From fabe0ae4f1bf26355edc0d82c3a0a660771f2dd7 Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Sat, 23 Nov 2024 06:13:46 +0000 Subject: [PATCH 18/77] chore(internal): fix compat model_dump method when warnings are passed --- src/agility/_compat.py | 3 ++- tests/test_models.py | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/agility/_compat.py b/src/agility/_compat.py index 4794129..df173f8 100644 --- a/src/agility/_compat.py +++ b/src/agility/_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 2169428..2b39471 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 7726d75a931a2d294ee94d82300df72b120de784 Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Sat, 23 Nov 2024 06:14:51 +0000 Subject: [PATCH 19/77] docs: add info log level to readme --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 64977d0..216f7fb 100644 --- a/README.md +++ b/README.md @@ -252,12 +252,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 `AGILITY_LOG` to `debug`. +You can enable logging by setting the environment variable `AGILITY_LOG` to `info`. ```shell -$ export AGILITY_LOG=debug +$ export AGILITY_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 80dbf03cc710a31f0ec10342dff09effa7552610 Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Tue, 26 Nov 2024 06:05:58 +0000 Subject: [PATCH 20/77] chore: remove now unused `cached-property` dep --- pyproject.toml | 1 - src/agility/_compat.py | 5 +---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c4a0415..94cb9ac 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/agility/_compat.py b/src/agility/_compat.py index df173f8..92d9ee6 100644 --- a/src/agility/_compat.py +++ b/src/agility/_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 3fbec567613fd5c17d1690af57ff4f840095a19d Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Thu, 28 Nov 2024 06:23:39 +0000 Subject: [PATCH 21/77] chore(internal): exclude mypy from running on tests --- mypy.ini | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mypy.ini b/mypy.ini index 7e0fc28..f353443 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/agility/_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/agility/_files\.py|_dev/.*\.py|tests/.*)$ strict_equality = True implicit_reexport = True From 8d804cd1a7202175b3d3630773765c5e76a2693c Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Thu, 28 Nov 2024 19:10:36 +0000 Subject: [PATCH 22/77] fix(client): compat with new httpx 0.28.0 release --- src/agility/_base_client.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/agility/_base_client.py b/src/agility/_base_client.py index 62e01ed..d4410a2 100644 --- a/src/agility/_base_client.py +++ b/src/agility/_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 0694d798d6971dcccee498be00171d36ca9cf1f8 Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Tue, 3 Dec 2024 00:17:39 +0000 Subject: [PATCH 23/77] feat(api): api update --- .stats.yml | 2 +- .../resources/assistants/assistants.py | 8 +-- src/agility/types/assistant_create_params.py | 2 +- src/agility/types/assistant_update_params.py | 2 +- src/agility/types/assistant_with_config.py | 2 +- src/agility/types/gc_sv0_integration.py | 4 +- src/agility/types/integration.py | 4 +- .../types/integration_create_params.py | 19 +++++-- .../types/integration_create_response.py | 51 +++++++++++++++++-- .../types/integration_list_response.py | 51 +++++++++++++++++-- .../types/integration_retrieve_response.py | 51 +++++++++++++++++-- .../integrations/integration_type_def.py | 4 +- src/agility/types/knowledge_bases/source.py | 12 +++-- .../knowledge_bases/source_create_params.py | 12 +++-- .../knowledge_bases/source_update_params.py | 12 +++-- src/agility/types/s3_v0_integration.py | 4 +- tests/api_resources/test_knowledge_bases.py | 36 ++++++------- 17 files changed, 215 insertions(+), 61 deletions(-) diff --git a/.stats.yml b/.stats.yml index b6ba801..2d1ebc4 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 42 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-d630ad236884869cf0899ddac1e8b99e0854f5fec3878ce31b7a6403e0a2791c.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-c3d745f0888a3b61476808927ca0765d31b39d47dc32a030043424516d44b89a.yml diff --git a/src/agility/resources/assistants/assistants.py b/src/agility/resources/assistants/assistants.py index 5ba8de9..1840c2c 100644 --- a/src/agility/resources/assistants/assistants.py +++ b/src/agility/resources/assistants/assistants.py @@ -65,7 +65,7 @@ def create( self, *, description: str, - knowledge_base_id: str, + knowledge_base_id: Optional[str], name: str, instructions: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, @@ -157,7 +157,7 @@ def update( *, id: str, description: str, - knowledge_base_id: str, + knowledge_base_id: Optional[str], name: str, instructions: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, @@ -319,7 +319,7 @@ async def create( self, *, description: str, - knowledge_base_id: str, + knowledge_base_id: Optional[str], name: str, instructions: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, @@ -411,7 +411,7 @@ async def update( *, id: str, description: str, - knowledge_base_id: str, + knowledge_base_id: Optional[str], name: str, instructions: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, diff --git a/src/agility/types/assistant_create_params.py b/src/agility/types/assistant_create_params.py index f47e919..5e694da 100644 --- a/src/agility/types/assistant_create_params.py +++ b/src/agility/types/assistant_create_params.py @@ -12,7 +12,7 @@ class AssistantCreateParams(TypedDict, total=False): description: Required[str] """The description of the assistant""" - knowledge_base_id: Required[str] + knowledge_base_id: Required[Optional[str]] name: Required[str] """The name of the assistant""" diff --git a/src/agility/types/assistant_update_params.py b/src/agility/types/assistant_update_params.py index 53c5912..3208db8 100644 --- a/src/agility/types/assistant_update_params.py +++ b/src/agility/types/assistant_update_params.py @@ -14,7 +14,7 @@ class AssistantUpdateParams(TypedDict, total=False): description: Required[str] """The description of the assistant""" - knowledge_base_id: Required[str] + knowledge_base_id: Required[Optional[str]] name: Required[str] """The name of the assistant""" diff --git a/src/agility/types/assistant_with_config.py b/src/agility/types/assistant_with_config.py index 7d903e9..d9aa530 100644 --- a/src/agility/types/assistant_with_config.py +++ b/src/agility/types/assistant_with_config.py @@ -19,7 +19,7 @@ class AssistantWithConfig(BaseModel): description: str """The description of the assistant""" - knowledge_base_id: str + knowledge_base_id: Optional[str] = None name: str """The name of the assistant""" diff --git a/src/agility/types/gc_sv0_integration.py b/src/agility/types/gc_sv0_integration.py index 71017f3..1ea9264 100644 --- a/src/agility/types/gc_sv0_integration.py +++ b/src/agility/types/gc_sv0_integration.py @@ -25,6 +25,6 @@ class GcSv0Integration(BaseModel): state: Literal["ready", "pending", "error"] - integration_category: Optional[Literal["rbac"]] = None + integration_category: Optional[Literal["rbac", "oauth"]] = None - integration_type: Optional[Literal["s3/v0", "gcs/v0"]] = None + integration_type: Optional[Literal["s3/v0", "gcs/v0", "notion/v0", "slack/v0"]] = None diff --git a/src/agility/types/integration.py b/src/agility/types/integration.py index 8f2c32b..0852daf 100644 --- a/src/agility/types/integration.py +++ b/src/agility/types/integration.py @@ -10,8 +10,8 @@ class Integration(BaseModel): id: str - integration_category: Literal["rbac"] + integration_category: Literal["rbac", "oauth"] - integration_type: Literal["s3/v0", "gcs/v0"] + integration_type: Literal["s3/v0", "gcs/v0", "notion/v0", "slack/v0"] state: Literal["ready", "pending", "error"] diff --git a/src/agility/types/integration_create_params.py b/src/agility/types/integration_create_params.py index f774b02..c17814a 100644 --- a/src/agility/types/integration_create_params.py +++ b/src/agility/types/integration_create_params.py @@ -12,6 +12,7 @@ "IntegrationParamsS3IntegrationParamsV0Resource", "IntegrationParamsGcsIntegrationParamsV0", "IntegrationParamsGcsIntegrationParamsV0Resource", + "IntegrationParamsNotionIntegrationParamsV0", ] @@ -31,7 +32,7 @@ class IntegrationParamsS3IntegrationParamsV0Resource(TypedDict, total=False): class IntegrationParamsS3IntegrationParamsV0(TypedDict, total=False): resource: Required[IntegrationParamsS3IntegrationParamsV0Resource] - integration_category: Literal["rbac"] + integration_category: Literal["rbac", "oauth"] integration_type: Literal["s3/v0"] @@ -43,9 +44,21 @@ class IntegrationParamsGcsIntegrationParamsV0Resource(TypedDict, total=False): class IntegrationParamsGcsIntegrationParamsV0(TypedDict, total=False): resource: Required[IntegrationParamsGcsIntegrationParamsV0Resource] - integration_category: Literal["rbac"] + integration_category: Literal["rbac", "oauth"] integration_type: Literal["gcs/v0"] -IntegrationParams: TypeAlias = Union[IntegrationParamsS3IntegrationParamsV0, IntegrationParamsGcsIntegrationParamsV0] +class IntegrationParamsNotionIntegrationParamsV0(TypedDict, total=False): + authorization_code: Required[str] + + integration_category: Literal["rbac", "oauth"] + + integration_type: Literal["notion/v0"] + + +IntegrationParams: TypeAlias = Union[ + IntegrationParamsS3IntegrationParamsV0, + IntegrationParamsGcsIntegrationParamsV0, + IntegrationParamsNotionIntegrationParamsV0, +] diff --git a/src/agility/types/integration_create_response.py b/src/agility/types/integration_create_response.py index 3c15227..91b0891 100644 --- a/src/agility/types/integration_create_response.py +++ b/src/agility/types/integration_create_response.py @@ -1,11 +1,54 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Union -from typing_extensions import TypeAlias +from typing import Union, Optional +from typing_extensions import Literal, TypeAlias +from .._models import BaseModel from .s3_v0_integration import S3V0Integration from .gc_sv0_integration import GcSv0Integration -__all__ = ["IntegrationCreateResponse"] +__all__ = [ + "IntegrationCreateResponse", + "NotionV0Integration", + "NotionV0IntegrationToken", + "NotionV0IntegrationTokenNotionAccessToken", + "NotionV0IntegrationTokenSlackAccessToken", +] -IntegrationCreateResponse: TypeAlias = Union[S3V0Integration, GcSv0Integration] + +class NotionV0IntegrationTokenNotionAccessToken(BaseModel): + access_token: str + + bot_id: str + + owner: object + + workspace_id: str + + integration_type: Optional[Literal["notion/v0"]] = None + + +class NotionV0IntegrationTokenSlackAccessToken(BaseModel): + access_token: str + + integration_type: Optional[Literal["slack/v0"]] = None + + +NotionV0IntegrationToken: TypeAlias = Union[ + NotionV0IntegrationTokenNotionAccessToken, NotionV0IntegrationTokenSlackAccessToken +] + + +class NotionV0Integration(BaseModel): + id: str + + token: NotionV0IntegrationToken + + state: Literal["ready", "pending", "error"] + + integration_category: Optional[Literal["rbac", "oauth"]] = None + + integration_type: Optional[Literal["s3/v0", "gcs/v0", "notion/v0", "slack/v0"]] = None + + +IntegrationCreateResponse: TypeAlias = Union[S3V0Integration, GcSv0Integration, NotionV0Integration] diff --git a/src/agility/types/integration_list_response.py b/src/agility/types/integration_list_response.py index a8b8b79..96b8e15 100644 --- a/src/agility/types/integration_list_response.py +++ b/src/agility/types/integration_list_response.py @@ -1,11 +1,54 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Union -from typing_extensions import TypeAlias +from typing import Union, Optional +from typing_extensions import Literal, TypeAlias +from .._models import BaseModel from .s3_v0_integration import S3V0Integration from .gc_sv0_integration import GcSv0Integration -__all__ = ["IntegrationListResponse"] +__all__ = [ + "IntegrationListResponse", + "NotionV0Integration", + "NotionV0IntegrationToken", + "NotionV0IntegrationTokenNotionAccessToken", + "NotionV0IntegrationTokenSlackAccessToken", +] -IntegrationListResponse: TypeAlias = Union[S3V0Integration, GcSv0Integration] + +class NotionV0IntegrationTokenNotionAccessToken(BaseModel): + access_token: str + + bot_id: str + + owner: object + + workspace_id: str + + integration_type: Optional[Literal["notion/v0"]] = None + + +class NotionV0IntegrationTokenSlackAccessToken(BaseModel): + access_token: str + + integration_type: Optional[Literal["slack/v0"]] = None + + +NotionV0IntegrationToken: TypeAlias = Union[ + NotionV0IntegrationTokenNotionAccessToken, NotionV0IntegrationTokenSlackAccessToken +] + + +class NotionV0Integration(BaseModel): + id: str + + token: NotionV0IntegrationToken + + state: Literal["ready", "pending", "error"] + + integration_category: Optional[Literal["rbac", "oauth"]] = None + + integration_type: Optional[Literal["s3/v0", "gcs/v0", "notion/v0", "slack/v0"]] = None + + +IntegrationListResponse: TypeAlias = Union[S3V0Integration, GcSv0Integration, NotionV0Integration] diff --git a/src/agility/types/integration_retrieve_response.py b/src/agility/types/integration_retrieve_response.py index f634a6f..aa3aef3 100644 --- a/src/agility/types/integration_retrieve_response.py +++ b/src/agility/types/integration_retrieve_response.py @@ -1,11 +1,54 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Union -from typing_extensions import TypeAlias +from typing import Union, Optional +from typing_extensions import Literal, TypeAlias +from .._models import BaseModel from .s3_v0_integration import S3V0Integration from .gc_sv0_integration import GcSv0Integration -__all__ = ["IntegrationRetrieveResponse"] +__all__ = [ + "IntegrationRetrieveResponse", + "NotionV0Integration", + "NotionV0IntegrationToken", + "NotionV0IntegrationTokenNotionAccessToken", + "NotionV0IntegrationTokenSlackAccessToken", +] -IntegrationRetrieveResponse: TypeAlias = Union[S3V0Integration, GcSv0Integration] + +class NotionV0IntegrationTokenNotionAccessToken(BaseModel): + access_token: str + + bot_id: str + + owner: object + + workspace_id: str + + integration_type: Optional[Literal["notion/v0"]] = None + + +class NotionV0IntegrationTokenSlackAccessToken(BaseModel): + access_token: str + + integration_type: Optional[Literal["slack/v0"]] = None + + +NotionV0IntegrationToken: TypeAlias = Union[ + NotionV0IntegrationTokenNotionAccessToken, NotionV0IntegrationTokenSlackAccessToken +] + + +class NotionV0Integration(BaseModel): + id: str + + token: NotionV0IntegrationToken + + state: Literal["ready", "pending", "error"] + + integration_category: Optional[Literal["rbac", "oauth"]] = None + + integration_type: Optional[Literal["s3/v0", "gcs/v0", "notion/v0", "slack/v0"]] = None + + +IntegrationRetrieveResponse: TypeAlias = Union[S3V0Integration, GcSv0Integration, NotionV0Integration] diff --git a/src/agility/types/integrations/integration_type_def.py b/src/agility/types/integrations/integration_type_def.py index 01b9d48..5cd473b 100644 --- a/src/agility/types/integrations/integration_type_def.py +++ b/src/agility/types/integrations/integration_type_def.py @@ -10,8 +10,8 @@ class IntegrationTypeDef(BaseModel): description: str - integration_category: Literal["rbac"] + integration_category: Literal["rbac", "oauth"] - integration_type: Literal["s3/v0", "gcs/v0"] + integration_type: Literal["s3/v0", "gcs/v0", "notion/v0", "slack/v0"] name: str diff --git a/src/agility/types/knowledge_bases/source.py b/src/agility/types/knowledge_bases/source.py index 028402f..e30de98 100644 --- a/src/agility/types/knowledge_bases/source.py +++ b/src/agility/types/knowledge_bases/source.py @@ -12,7 +12,7 @@ "SourceParams", "SourceParamsWebV0Params", "SourceParamsWebV0ParamsScrapeOptions", - "SourceParamsNotionParams", + "SourceParamsNotionV0Params", "SourceParamsS3PublicV0Params", "SourceParamsS3PrivateV0Params", "SourceSchedule", @@ -48,8 +48,12 @@ class SourceParamsWebV0Params(BaseModel): """Parameters for scraping each crawled page.""" -class SourceParamsNotionParams(BaseModel): - name: Optional[Literal["notion"]] = None +class SourceParamsNotionV0Params(BaseModel): + integration_id: str + + limit: Optional[int] = None + + name: Optional[Literal["notion_v0"]] = None class SourceParamsS3PublicV0Params(BaseModel): @@ -76,7 +80,7 @@ class SourceParamsS3PrivateV0Params(BaseModel): SourceParams: TypeAlias = Annotated[ Union[ - SourceParamsWebV0Params, SourceParamsNotionParams, SourceParamsS3PublicV0Params, SourceParamsS3PrivateV0Params + SourceParamsWebV0Params, SourceParamsNotionV0Params, SourceParamsS3PublicV0Params, SourceParamsS3PrivateV0Params ], PropertyInfo(discriminator="name"), ] diff --git a/src/agility/types/knowledge_bases/source_create_params.py b/src/agility/types/knowledge_bases/source_create_params.py index 6450436..bd5ee46 100644 --- a/src/agility/types/knowledge_bases/source_create_params.py +++ b/src/agility/types/knowledge_bases/source_create_params.py @@ -10,7 +10,7 @@ "SourceParams", "SourceParamsWebV0Params", "SourceParamsWebV0ParamsScrapeOptions", - "SourceParamsNotionParams", + "SourceParamsNotionV0Params", "SourceParamsS3PublicV0Params", "SourceParamsS3PrivateV0Params", "SourceSchedule", @@ -60,8 +60,12 @@ class SourceParamsWebV0Params(TypedDict, total=False): """Parameters for scraping each crawled page.""" -class SourceParamsNotionParams(TypedDict, total=False): - name: Literal["notion"] +class SourceParamsNotionV0Params(TypedDict, total=False): + integration_id: Required[str] + + limit: Optional[int] + + name: Literal["notion_v0"] class SourceParamsS3PublicV0Params(TypedDict, total=False): @@ -87,7 +91,7 @@ class SourceParamsS3PrivateV0Params(TypedDict, total=False): SourceParams: TypeAlias = Union[ - SourceParamsWebV0Params, SourceParamsNotionParams, SourceParamsS3PublicV0Params, SourceParamsS3PrivateV0Params + SourceParamsWebV0Params, SourceParamsNotionV0Params, SourceParamsS3PublicV0Params, SourceParamsS3PrivateV0Params ] diff --git a/src/agility/types/knowledge_bases/source_update_params.py b/src/agility/types/knowledge_bases/source_update_params.py index 7c29a51..4f34c35 100644 --- a/src/agility/types/knowledge_bases/source_update_params.py +++ b/src/agility/types/knowledge_bases/source_update_params.py @@ -10,7 +10,7 @@ "SourceParams", "SourceParamsWebV0Params", "SourceParamsWebV0ParamsScrapeOptions", - "SourceParamsNotionParams", + "SourceParamsNotionV0Params", "SourceParamsS3PublicV0Params", "SourceParamsS3PrivateV0Params", "SourceSchedule", @@ -62,8 +62,12 @@ class SourceParamsWebV0Params(TypedDict, total=False): """Parameters for scraping each crawled page.""" -class SourceParamsNotionParams(TypedDict, total=False): - name: Literal["notion"] +class SourceParamsNotionV0Params(TypedDict, total=False): + integration_id: Required[str] + + limit: Optional[int] + + name: Literal["notion_v0"] class SourceParamsS3PublicV0Params(TypedDict, total=False): @@ -89,7 +93,7 @@ class SourceParamsS3PrivateV0Params(TypedDict, total=False): SourceParams: TypeAlias = Union[ - SourceParamsWebV0Params, SourceParamsNotionParams, SourceParamsS3PublicV0Params, SourceParamsS3PrivateV0Params + SourceParamsWebV0Params, SourceParamsNotionV0Params, SourceParamsS3PublicV0Params, SourceParamsS3PrivateV0Params ] diff --git a/src/agility/types/s3_v0_integration.py b/src/agility/types/s3_v0_integration.py index ee0751a..e9b09f9 100644 --- a/src/agility/types/s3_v0_integration.py +++ b/src/agility/types/s3_v0_integration.py @@ -31,6 +31,6 @@ class S3V0Integration(BaseModel): state: Literal["ready", "pending", "error"] - integration_category: Optional[Literal["rbac"]] = None + integration_category: Optional[Literal["rbac", "oauth"]] = None - integration_type: Optional[Literal["s3/v0", "gcs/v0"]] = None + integration_type: Optional[Literal["s3/v0", "gcs/v0", "notion/v0", "slack/v0"]] = None diff --git a/tests/api_resources/test_knowledge_bases.py b/tests/api_resources/test_knowledge_bases.py index 3726d9e..34a0eca 100644 --- a/tests/api_resources/test_knowledge_bases.py +++ b/tests/api_resources/test_knowledge_bases.py @@ -75,14 +75,14 @@ def test_streaming_response_create(self, client: Agility) -> None: @parametrize def test_method_retrieve(self, client: Agility) -> None: knowledge_base = client.knowledge_bases.retrieve( - "knowledge_base_id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert_matches_type(KnowledgeBaseWithConfig, knowledge_base, path=["response"]) @parametrize def test_raw_response_retrieve(self, client: Agility) -> None: response = client.knowledge_bases.with_raw_response.retrieve( - "knowledge_base_id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert response.is_closed is True @@ -93,7 +93,7 @@ def test_raw_response_retrieve(self, client: Agility) -> None: @parametrize def test_streaming_response_retrieve(self, client: Agility) -> None: with client.knowledge_bases.with_streaming_response.retrieve( - "knowledge_base_id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -113,7 +113,7 @@ def test_path_params_retrieve(self, client: Agility) -> None: @parametrize def test_method_update(self, client: Agility) -> None: knowledge_base = client.knowledge_bases.update( - knowledge_base_id="knowledge_base_id", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", ingestion_pipeline_params={ "curate": {}, @@ -128,7 +128,7 @@ def test_method_update(self, client: Agility) -> None: @parametrize def test_raw_response_update(self, client: Agility) -> None: response = client.knowledge_bases.with_raw_response.update( - knowledge_base_id="knowledge_base_id", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", ingestion_pipeline_params={ "curate": {}, @@ -147,7 +147,7 @@ def test_raw_response_update(self, client: Agility) -> None: @parametrize def test_streaming_response_update(self, client: Agility) -> None: with client.knowledge_bases.with_streaming_response.update( - knowledge_base_id="knowledge_base_id", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", ingestion_pipeline_params={ "curate": {}, @@ -216,14 +216,14 @@ def test_streaming_response_list(self, client: Agility) -> None: @parametrize def test_method_delete(self, client: Agility) -> None: knowledge_base = client.knowledge_bases.delete( - "knowledge_base_id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert knowledge_base is None @parametrize def test_raw_response_delete(self, client: Agility) -> None: response = client.knowledge_bases.with_raw_response.delete( - "knowledge_base_id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert response.is_closed is True @@ -234,7 +234,7 @@ def test_raw_response_delete(self, client: Agility) -> None: @parametrize def test_streaming_response_delete(self, client: Agility) -> None: with client.knowledge_bases.with_streaming_response.delete( - "knowledge_base_id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -310,14 +310,14 @@ async def test_streaming_response_create(self, async_client: AsyncAgility) -> No @parametrize async def test_method_retrieve(self, async_client: AsyncAgility) -> None: knowledge_base = await async_client.knowledge_bases.retrieve( - "knowledge_base_id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert_matches_type(KnowledgeBaseWithConfig, knowledge_base, path=["response"]) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncAgility) -> None: response = await async_client.knowledge_bases.with_raw_response.retrieve( - "knowledge_base_id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert response.is_closed is True @@ -328,7 +328,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncAgility) -> None: @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncAgility) -> None: async with async_client.knowledge_bases.with_streaming_response.retrieve( - "knowledge_base_id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -348,7 +348,7 @@ async def test_path_params_retrieve(self, async_client: AsyncAgility) -> None: @parametrize async def test_method_update(self, async_client: AsyncAgility) -> None: knowledge_base = await async_client.knowledge_bases.update( - knowledge_base_id="knowledge_base_id", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", ingestion_pipeline_params={ "curate": {}, @@ -363,7 +363,7 @@ async def test_method_update(self, async_client: AsyncAgility) -> None: @parametrize async def test_raw_response_update(self, async_client: AsyncAgility) -> None: response = await async_client.knowledge_bases.with_raw_response.update( - knowledge_base_id="knowledge_base_id", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", ingestion_pipeline_params={ "curate": {}, @@ -382,7 +382,7 @@ async def test_raw_response_update(self, async_client: AsyncAgility) -> None: @parametrize async def test_streaming_response_update(self, async_client: AsyncAgility) -> None: async with async_client.knowledge_bases.with_streaming_response.update( - knowledge_base_id="knowledge_base_id", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", ingestion_pipeline_params={ "curate": {}, @@ -451,14 +451,14 @@ async def test_streaming_response_list(self, async_client: AsyncAgility) -> None @parametrize async def test_method_delete(self, async_client: AsyncAgility) -> None: knowledge_base = await async_client.knowledge_bases.delete( - "knowledge_base_id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert knowledge_base is None @parametrize async def test_raw_response_delete(self, async_client: AsyncAgility) -> None: response = await async_client.knowledge_bases.with_raw_response.delete( - "knowledge_base_id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) assert response.is_closed is True @@ -469,7 +469,7 @@ async def test_raw_response_delete(self, async_client: AsyncAgility) -> None: @parametrize async def test_streaming_response_delete(self, async_client: AsyncAgility) -> None: async with async_client.knowledge_bases.with_streaming_response.delete( - "knowledge_base_id", + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" From 89681da886900796c28da235f2578f7a9d0871f7 Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Tue, 3 Dec 2024 06:40:06 +0000 Subject: [PATCH 24/77] chore(internal): bump pyright --- requirements-dev.lock | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements-dev.lock b/requirements-dev.lock index 46ba92e..c4a884a 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 @@ -97,6 +97,7 @@ typing-extensions==4.12.2 # via mypy # via pydantic # via pydantic-core + # via pyright virtualenv==20.24.5 # via nox zipp==3.17.0 From 0f2da323aa06451fb286279bfe4439d4cda49b26 Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Wed, 4 Dec 2024 06:42:14 +0000 Subject: [PATCH 25/77] chore: make the `Omit` type public --- src/agility/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/agility/__init__.py b/src/agility/__init__.py index 1e77292..ec35654 100644 --- a/src/agility/__init__.py +++ b/src/agility/__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 ( ENVIRONMENTS, @@ -47,6 +47,7 @@ "ProxiesTypes", "NotGiven", "NOT_GIVEN", + "Omit", "AgilityError", "APIError", "APIStatusError", From b12076fe2019556131ef86d59b13ccbd376cf936 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 08:17:51 +0000 Subject: [PATCH 26/77] feat(api): api update --- .stats.yml | 2 +- api.md | 4 +- .../resources/assistants/assistants.py | 13 ++++--- src/agility/types/__init__.py | 1 + src/agility/types/assistant_list_response.py | 39 +++++++++++++++++++ src/agility/types/knowledge_bases/source.py | 24 +++++++++++- .../knowledge_bases/source_create_params.py | 24 +++++++++++- .../knowledge_bases/source_update_params.py | 24 +++++++++++- .../knowledge_bases/test_sources.py | 28 +++++++++++-- tests/api_resources/test_assistants.py | 17 ++++---- 10 files changed, 152 insertions(+), 24 deletions(-) create mode 100644 src/agility/types/assistant_list_response.py diff --git a/.stats.yml b/.stats.yml index 2d1ebc4..140e083 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 42 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-c3d745f0888a3b61476808927ca0765d31b39d47dc32a030043424516d44b89a.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-df1d1cc9e050274d0ab0fd61a51034fccdf5006ef8bad08e852a06e28f5b79b0.yml diff --git a/api.md b/api.md index d86da6f..a7a6def 100644 --- a/api.md +++ b/api.md @@ -3,7 +3,7 @@ Types: ```python -from agility.types import Assistant, AssistantWithConfig +from agility.types import Assistant, AssistantWithConfig, AssistantListResponse ``` Methods: @@ -11,7 +11,7 @@ Methods: - client.assistants.create(\*\*params) -> Assistant - client.assistants.retrieve(assistant_id) -> AssistantWithConfig - client.assistants.update(assistant_id, \*\*params) -> AssistantWithConfig -- client.assistants.list(\*\*params) -> SyncMyOffsetPage[AssistantWithConfig] +- client.assistants.list(\*\*params) -> SyncMyOffsetPage[AssistantListResponse] - client.assistants.delete(assistant_id) -> None ## AccessKeys diff --git a/src/agility/resources/assistants/assistants.py b/src/agility/resources/assistants/assistants.py index 1840c2c..f484f70 100644 --- a/src/agility/resources/assistants/assistants.py +++ b/src/agility/resources/assistants/assistants.py @@ -33,6 +33,7 @@ from ..._base_client import AsyncPaginator, make_request_options from ...types.assistant import Assistant from ...types.assistant_with_config import AssistantWithConfig +from ...types.assistant_list_response import AssistantListResponse __all__ = ["AssistantsResource", "AsyncAssistantsResource"] @@ -224,7 +225,7 @@ def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SyncMyOffsetPage[AssistantWithConfig]: + ) -> SyncMyOffsetPage[AssistantListResponse]: """ Get all assistants for the current user. @@ -239,7 +240,7 @@ def list( """ return self._get_api_list( "/api/assistants/", - page=SyncMyOffsetPage[AssistantWithConfig], + page=SyncMyOffsetPage[AssistantListResponse], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -253,7 +254,7 @@ def list( assistant_list_params.AssistantListParams, ), ), - model=AssistantWithConfig, + model=AssistantListResponse, ) def delete( @@ -478,7 +479,7 @@ def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AsyncPaginator[AssistantWithConfig, AsyncMyOffsetPage[AssistantWithConfig]]: + ) -> AsyncPaginator[AssistantListResponse, AsyncMyOffsetPage[AssistantListResponse]]: """ Get all assistants for the current user. @@ -493,7 +494,7 @@ def list( """ return self._get_api_list( "/api/assistants/", - page=AsyncMyOffsetPage[AssistantWithConfig], + page=AsyncMyOffsetPage[AssistantListResponse], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -507,7 +508,7 @@ def list( assistant_list_params.AssistantListParams, ), ), - model=AssistantWithConfig, + model=AssistantListResponse, ) async def delete( diff --git a/src/agility/types/__init__.py b/src/agility/types/__init__.py index ef19224..29bd181 100644 --- a/src/agility/types/__init__.py +++ b/src/agility/types/__init__.py @@ -12,6 +12,7 @@ from .assistant_list_params import AssistantListParams as AssistantListParams from .assistant_with_config import AssistantWithConfig as AssistantWithConfig from .assistant_create_params import AssistantCreateParams as AssistantCreateParams +from .assistant_list_response import AssistantListResponse as AssistantListResponse from .assistant_update_params import AssistantUpdateParams as AssistantUpdateParams from .integration_list_params import IntegrationListParams as IntegrationListParams from .integration_create_params import IntegrationCreateParams as IntegrationCreateParams diff --git a/src/agility/types/assistant_list_response.py b/src/agility/types/assistant_list_response.py new file mode 100644 index 0000000..0d81886 --- /dev/null +++ b/src/agility/types/assistant_list_response.py @@ -0,0 +1,39 @@ +# 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__ = ["AssistantListResponse"] + + +class AssistantListResponse(BaseModel): + id: str + + created_at: datetime + + deleted_at: Optional[datetime] = None + + description: str + """The description of the assistant""" + + knowledge_base_id: Optional[str] = None + + knowledge_base_name: str + + name: str + """The name of the assistant""" + + updated_at: datetime + + instructions: Optional[str] = None + + model: Optional[Literal["gpt-4o"]] = None + + suggested_questions: Optional[List[str]] = None + """A list of suggested questions that can be asked to the assistant""" + + url_slug: Optional[str] = None + """Optional URL suffix - unique identifier for the assistant's endpoint""" diff --git a/src/agility/types/knowledge_bases/source.py b/src/agility/types/knowledge_bases/source.py index e30de98..7050d5d 100644 --- a/src/agility/types/knowledge_bases/source.py +++ b/src/agility/types/knowledge_bases/source.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List, Union, Optional +from typing import Dict, List, Union, Optional from datetime import datetime from typing_extensions import Literal, Annotated, TypeAlias @@ -20,6 +20,18 @@ class SourceParamsWebV0ParamsScrapeOptions(BaseModel): + headers: Optional[Dict[str, str]] = None + """HTTP headers to send with each request. + + Can be used to send cookies, user-agent, etc. + """ + + only_main_content: Optional[bool] = None + """ + Whether to only scrape the main content of the page (excluding headers, navs, + footers, etc.). + """ + wait_for: Optional[int] = None """ Amount of time (in milliseconds) to wait for each page to load before scraping @@ -29,18 +41,28 @@ class SourceParamsWebV0ParamsScrapeOptions(BaseModel): class SourceParamsWebV0Params(BaseModel): urls: List[str] + """List of URLs to crawl.""" allow_backward_links: Optional[bool] = None + """Whether to allow the crawler to navigate backwards from the given URL.""" allow_external_links: Optional[bool] = None + """Whether to allow the crawler to follow links to external websites.""" exclude_regex: Optional[str] = None + """Regex pattern to exclude URLs that match the pattern.""" + + ignore_sitemap: Optional[bool] = None + """Whether to ignore the website sitemap when crawling.""" include_regex: Optional[str] = None + """Regex pattern to include URLs that match the pattern.""" limit: Optional[int] = None + """Maximum number of pages to crawl per URL.""" max_depth: Optional[int] = None + """Maximum depth of pages to crawl relative to the root URL.""" name: Optional[Literal["web_v0"]] = None diff --git a/src/agility/types/knowledge_bases/source_create_params.py b/src/agility/types/knowledge_bases/source_create_params.py index bd5ee46..5908821 100644 --- a/src/agility/types/knowledge_bases/source_create_params.py +++ b/src/agility/types/knowledge_bases/source_create_params.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import List, Union, Optional +from typing import Dict, List, Union, Optional from typing_extensions import Literal, Required, TypeAlias, TypedDict __all__ = [ @@ -32,6 +32,18 @@ class SourceCreateParams(TypedDict, total=False): class SourceParamsWebV0ParamsScrapeOptions(TypedDict, total=False): + headers: Dict[str, str] + """HTTP headers to send with each request. + + Can be used to send cookies, user-agent, etc. + """ + + only_main_content: bool + """ + Whether to only scrape the main content of the page (excluding headers, navs, + footers, etc.). + """ + wait_for: int """ Amount of time (in milliseconds) to wait for each page to load before scraping @@ -41,18 +53,28 @@ class SourceParamsWebV0ParamsScrapeOptions(TypedDict, total=False): class SourceParamsWebV0Params(TypedDict, total=False): urls: Required[List[str]] + """List of URLs to crawl.""" allow_backward_links: bool + """Whether to allow the crawler to navigate backwards from the given URL.""" allow_external_links: bool + """Whether to allow the crawler to follow links to external websites.""" exclude_regex: Optional[str] + """Regex pattern to exclude URLs that match the pattern.""" + + ignore_sitemap: bool + """Whether to ignore the website sitemap when crawling.""" include_regex: Optional[str] + """Regex pattern to include URLs that match the pattern.""" limit: int + """Maximum number of pages to crawl per URL.""" max_depth: int + """Maximum depth of pages to crawl relative to the root URL.""" name: Literal["web_v0"] diff --git a/src/agility/types/knowledge_bases/source_update_params.py b/src/agility/types/knowledge_bases/source_update_params.py index 4f34c35..9d582e0 100644 --- a/src/agility/types/knowledge_bases/source_update_params.py +++ b/src/agility/types/knowledge_bases/source_update_params.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import List, Union, Optional +from typing import Dict, List, Union, Optional from typing_extensions import Literal, Required, TypeAlias, TypedDict __all__ = [ @@ -34,6 +34,18 @@ class SourceUpdateParams(TypedDict, total=False): class SourceParamsWebV0ParamsScrapeOptions(TypedDict, total=False): + headers: Dict[str, str] + """HTTP headers to send with each request. + + Can be used to send cookies, user-agent, etc. + """ + + only_main_content: bool + """ + Whether to only scrape the main content of the page (excluding headers, navs, + footers, etc.). + """ + wait_for: int """ Amount of time (in milliseconds) to wait for each page to load before scraping @@ -43,18 +55,28 @@ class SourceParamsWebV0ParamsScrapeOptions(TypedDict, total=False): class SourceParamsWebV0Params(TypedDict, total=False): urls: Required[List[str]] + """List of URLs to crawl.""" allow_backward_links: bool + """Whether to allow the crawler to navigate backwards from the given URL.""" allow_external_links: bool + """Whether to allow the crawler to follow links to external websites.""" exclude_regex: Optional[str] + """Regex pattern to exclude URLs that match the pattern.""" + + ignore_sitemap: bool + """Whether to ignore the website sitemap when crawling.""" include_regex: Optional[str] + """Regex pattern to include URLs that match the pattern.""" limit: int + """Maximum number of pages to crawl per URL.""" max_depth: int + """Maximum depth of pages to crawl relative to the root URL.""" name: Literal["web_v0"] diff --git a/tests/api_resources/knowledge_bases/test_sources.py b/tests/api_resources/knowledge_bases/test_sources.py index 7a854b9..f4bcffa 100644 --- a/tests/api_resources/knowledge_bases/test_sources.py +++ b/tests/api_resources/knowledge_bases/test_sources.py @@ -46,11 +46,16 @@ def test_method_create_with_all_params(self, client: Agility) -> None: "allow_backward_links": True, "allow_external_links": True, "exclude_regex": "exclude_regex", + "ignore_sitemap": True, "include_regex": "include_regex", "limit": 0, "max_depth": 0, "name": "web_v0", - "scrape_options": {"wait_for": 0}, + "scrape_options": { + "headers": {"foo": "string"}, + "only_main_content": True, + "wait_for": 0, + }, }, source_schedule={ "cron": "cron", @@ -187,11 +192,16 @@ def test_method_update_with_all_params(self, client: Agility) -> None: "allow_backward_links": True, "allow_external_links": True, "exclude_regex": "exclude_regex", + "ignore_sitemap": True, "include_regex": "include_regex", "limit": 0, "max_depth": 0, "name": "web_v0", - "scrape_options": {"wait_for": 0}, + "scrape_options": { + "headers": {"foo": "string"}, + "only_main_content": True, + "wait_for": 0, + }, }, source_schedule={ "cron": "cron", @@ -489,11 +499,16 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgility) - "allow_backward_links": True, "allow_external_links": True, "exclude_regex": "exclude_regex", + "ignore_sitemap": True, "include_regex": "include_regex", "limit": 0, "max_depth": 0, "name": "web_v0", - "scrape_options": {"wait_for": 0}, + "scrape_options": { + "headers": {"foo": "string"}, + "only_main_content": True, + "wait_for": 0, + }, }, source_schedule={ "cron": "cron", @@ -630,11 +645,16 @@ async def test_method_update_with_all_params(self, async_client: AsyncAgility) - "allow_backward_links": True, "allow_external_links": True, "exclude_regex": "exclude_regex", + "ignore_sitemap": True, "include_regex": "include_regex", "limit": 0, "max_depth": 0, "name": "web_v0", - "scrape_options": {"wait_for": 0}, + "scrape_options": { + "headers": {"foo": "string"}, + "only_main_content": True, + "wait_for": 0, + }, }, source_schedule={ "cron": "cron", diff --git a/tests/api_resources/test_assistants.py b/tests/api_resources/test_assistants.py index d29c7ee..f4d2871 100644 --- a/tests/api_resources/test_assistants.py +++ b/tests/api_resources/test_assistants.py @@ -12,6 +12,7 @@ from agility.types import ( Assistant, AssistantWithConfig, + AssistantListResponse, ) from agility.pagination import SyncMyOffsetPage, AsyncMyOffsetPage @@ -181,7 +182,7 @@ def test_path_params_update(self, client: Agility) -> None: @parametrize def test_method_list(self, client: Agility) -> None: assistant = client.assistants.list() - assert_matches_type(SyncMyOffsetPage[AssistantWithConfig], assistant, path=["response"]) + assert_matches_type(SyncMyOffsetPage[AssistantListResponse], assistant, path=["response"]) @parametrize def test_method_list_with_all_params(self, client: Agility) -> None: @@ -189,7 +190,7 @@ def test_method_list_with_all_params(self, client: Agility) -> None: limit=1, offset=0, ) - assert_matches_type(SyncMyOffsetPage[AssistantWithConfig], assistant, path=["response"]) + assert_matches_type(SyncMyOffsetPage[AssistantListResponse], assistant, path=["response"]) @parametrize def test_raw_response_list(self, client: Agility) -> None: @@ -198,7 +199,7 @@ def test_raw_response_list(self, client: Agility) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" assistant = response.parse() - assert_matches_type(SyncMyOffsetPage[AssistantWithConfig], assistant, path=["response"]) + assert_matches_type(SyncMyOffsetPage[AssistantListResponse], assistant, path=["response"]) @parametrize def test_streaming_response_list(self, client: Agility) -> None: @@ -207,7 +208,7 @@ def test_streaming_response_list(self, client: Agility) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" assistant = response.parse() - assert_matches_type(SyncMyOffsetPage[AssistantWithConfig], assistant, path=["response"]) + assert_matches_type(SyncMyOffsetPage[AssistantListResponse], assistant, path=["response"]) assert cast(Any, response.is_closed) is True @@ -413,7 +414,7 @@ async def test_path_params_update(self, async_client: AsyncAgility) -> None: @parametrize async def test_method_list(self, async_client: AsyncAgility) -> None: assistant = await async_client.assistants.list() - assert_matches_type(AsyncMyOffsetPage[AssistantWithConfig], assistant, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[AssistantListResponse], assistant, path=["response"]) @parametrize async def test_method_list_with_all_params(self, async_client: AsyncAgility) -> None: @@ -421,7 +422,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncAgility) -> limit=1, offset=0, ) - assert_matches_type(AsyncMyOffsetPage[AssistantWithConfig], assistant, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[AssistantListResponse], assistant, path=["response"]) @parametrize async def test_raw_response_list(self, async_client: AsyncAgility) -> None: @@ -430,7 +431,7 @@ async def test_raw_response_list(self, async_client: AsyncAgility) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" assistant = await response.parse() - assert_matches_type(AsyncMyOffsetPage[AssistantWithConfig], assistant, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[AssistantListResponse], assistant, path=["response"]) @parametrize async def test_streaming_response_list(self, async_client: AsyncAgility) -> None: @@ -439,7 +440,7 @@ async def test_streaming_response_list(self, async_client: AsyncAgility) -> None assert response.http_request.headers.get("X-Stainless-Lang") == "python" assistant = await response.parse() - assert_matches_type(AsyncMyOffsetPage[AssistantWithConfig], assistant, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[AssistantListResponse], assistant, path=["response"]) assert cast(Any, response.is_closed) is True From afbd11d493269f73858867976d8a3ea24de41a51 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 7 Dec 2024 05:17:46 +0000 Subject: [PATCH 27/77] feat(api): api update --- .stats.yml | 2 +- .../types/knowledge_base_create_params.py | 21 +++++++++++++++++++ .../types/knowledge_base_update_params.py | 21 +++++++++++++++++++ .../types/knowledge_base_with_config.py | 21 +++++++++++++++++++ 4 files changed, 64 insertions(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 140e083..10d233f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 42 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-df1d1cc9e050274d0ab0fd61a51034fccdf5006ef8bad08e852a06e28f5b79b0.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-043c2e8fdb798983c2f46d3e1a7bebcb6dcca5f0e92cf7ecfc9de02c6c27f55a.yml diff --git a/src/agility/types/knowledge_base_create_params.py b/src/agility/types/knowledge_base_create_params.py index 71ce009..2ce6be4 100644 --- a/src/agility/types/knowledge_base_create_params.py +++ b/src/agility/types/knowledge_base_create_params.py @@ -19,6 +19,7 @@ "IngestionPipelineParamsTransformSteps", "IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params", "IngestionPipelineParamsTransformStepsSemanticMergeSplitterV0Params", + "IngestionPipelineParamsTransformStepsMarkdownNodeExpanderParams", "IngestionPipelineParamsTransformStepsNodeSummarizerV0Params", "IngestionPipelineParamsTransformStepsNoopParams", "IngestionPipelineParamsVectorStore", @@ -94,6 +95,25 @@ class IngestionPipelineParamsTransformStepsSemanticMergeSplitterV0Params(TypedDi name: Literal["splitters.semantic_merge.v0"] +class IngestionPipelineParamsTransformStepsMarkdownNodeExpanderParams(TypedDict, total=False): + code_block_pattern: str + """A regex pattern used to identify code blocks in markdown. + + Matches both multi-line code blocks enclosed in triple backticks and inline code + wrapped in single backticks. + """ + + name: Literal["node_expander.v0"] + """The version identifier for the node expander.""" + + section_delimiter_pattern: str + """A regex pattern used to identify markdown sections. + + Matches headers of level 1 to 6, capturing the section title and content until + the next header. + """ + + class IngestionPipelineParamsTransformStepsNodeSummarizerV0Params(TypedDict, total=False): expected_summary_tokens: int @@ -111,6 +131,7 @@ class IngestionPipelineParamsTransformStepsNoopParams(TypedDict, total=False): IngestionPipelineParamsTransformSteps: TypeAlias = Union[ IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params, IngestionPipelineParamsTransformStepsSemanticMergeSplitterV0Params, + IngestionPipelineParamsTransformStepsMarkdownNodeExpanderParams, IngestionPipelineParamsTransformStepsNodeSummarizerV0Params, IngestionPipelineParamsTransformStepsNoopParams, ] diff --git a/src/agility/types/knowledge_base_update_params.py b/src/agility/types/knowledge_base_update_params.py index 2582fc1..3c899d2 100644 --- a/src/agility/types/knowledge_base_update_params.py +++ b/src/agility/types/knowledge_base_update_params.py @@ -19,6 +19,7 @@ "IngestionPipelineParamsTransformSteps", "IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params", "IngestionPipelineParamsTransformStepsSemanticMergeSplitterV0Params", + "IngestionPipelineParamsTransformStepsMarkdownNodeExpanderParams", "IngestionPipelineParamsTransformStepsNodeSummarizerV0Params", "IngestionPipelineParamsTransformStepsNoopParams", "IngestionPipelineParamsVectorStore", @@ -94,6 +95,25 @@ class IngestionPipelineParamsTransformStepsSemanticMergeSplitterV0Params(TypedDi name: Literal["splitters.semantic_merge.v0"] +class IngestionPipelineParamsTransformStepsMarkdownNodeExpanderParams(TypedDict, total=False): + code_block_pattern: str + """A regex pattern used to identify code blocks in markdown. + + Matches both multi-line code blocks enclosed in triple backticks and inline code + wrapped in single backticks. + """ + + name: Literal["node_expander.v0"] + """The version identifier for the node expander.""" + + section_delimiter_pattern: str + """A regex pattern used to identify markdown sections. + + Matches headers of level 1 to 6, capturing the section title and content until + the next header. + """ + + class IngestionPipelineParamsTransformStepsNodeSummarizerV0Params(TypedDict, total=False): expected_summary_tokens: int @@ -111,6 +131,7 @@ class IngestionPipelineParamsTransformStepsNoopParams(TypedDict, total=False): IngestionPipelineParamsTransformSteps: TypeAlias = Union[ IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params, IngestionPipelineParamsTransformStepsSemanticMergeSplitterV0Params, + IngestionPipelineParamsTransformStepsMarkdownNodeExpanderParams, IngestionPipelineParamsTransformStepsNodeSummarizerV0Params, IngestionPipelineParamsTransformStepsNoopParams, ] diff --git a/src/agility/types/knowledge_base_with_config.py b/src/agility/types/knowledge_base_with_config.py index ae9f8d7..61365c7 100644 --- a/src/agility/types/knowledge_base_with_config.py +++ b/src/agility/types/knowledge_base_with_config.py @@ -21,6 +21,7 @@ "IngestionPipelineParamsTransformSteps", "IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params", "IngestionPipelineParamsTransformStepsSemanticMergeSplitterV0Params", + "IngestionPipelineParamsTransformStepsMarkdownNodeExpanderParams", "IngestionPipelineParamsTransformStepsNodeSummarizerV0Params", "IngestionPipelineParamsTransformStepsNoopParams", "IngestionPipelineParamsVectorStore", @@ -87,6 +88,25 @@ class IngestionPipelineParamsTransformStepsSemanticMergeSplitterV0Params(BaseMod name: Optional[Literal["splitters.semantic_merge.v0"]] = None +class IngestionPipelineParamsTransformStepsMarkdownNodeExpanderParams(BaseModel): + code_block_pattern: Optional[str] = None + """A regex pattern used to identify code blocks in markdown. + + Matches both multi-line code blocks enclosed in triple backticks and inline code + wrapped in single backticks. + """ + + name: Optional[Literal["node_expander.v0"]] = None + """The version identifier for the node expander.""" + + section_delimiter_pattern: Optional[str] = None + """A regex pattern used to identify markdown sections. + + Matches headers of level 1 to 6, capturing the section title and content until + the next header. + """ + + class IngestionPipelineParamsTransformStepsNodeSummarizerV0Params(BaseModel): expected_summary_tokens: Optional[int] = None @@ -104,6 +124,7 @@ class IngestionPipelineParamsTransformStepsNoopParams(BaseModel): IngestionPipelineParamsTransformSteps: TypeAlias = Union[ IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params, IngestionPipelineParamsTransformStepsSemanticMergeSplitterV0Params, + IngestionPipelineParamsTransformStepsMarkdownNodeExpanderParams, IngestionPipelineParamsTransformStepsNodeSummarizerV0Params, IngestionPipelineParamsTransformStepsNoopParams, ] From f12a797e5adbc45ebf40fa6b06a2c017cb51fe36 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 06:07:41 +0000 Subject: [PATCH 28/77] chore(internal): bump pydantic dependency --- requirements-dev.lock | 4 ++-- requirements.lock | 4 ++-- src/agility/_types.py | 6 ++---- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/requirements-dev.lock b/requirements-dev.lock index c4a884a..8557979 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 agility -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 0c055e5..034dcf4 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 agility -pydantic-core==2.23.4 +pydantic-core==2.27.1 # via pydantic sniffio==1.3.0 # via agility diff --git a/src/agility/_types.py b/src/agility/_types.py index ccfc20c..caac259 100644 --- a/src/agility/_types.py +++ b/src/agility/_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 f5980607179669293862254380332faa9e62f8bf Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 06:12:18 +0000 Subject: [PATCH 29/77] docs(readme): fix http client proxies example --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 216f7fb..cc9016c 100644 --- a/README.md +++ b/README.md @@ -354,18 +354,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 agility import Agility, DefaultHttpxClient client = Agility( # Or use the `AGILITY_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 06c99f7a55d54bcf6f9ca1696d2b882248e3ef8b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 18:17:54 +0000 Subject: [PATCH 30/77] feat(api): api update --- .stats.yml | 2 +- src/agility/types/threads/message.py | 2 ++ .../types/threads/message_create_params.py | 4 +++- .../types/threads/run_create_params.py | 4 +++- .../types/threads/run_stream_params.py | 4 +++- tests/api_resources/threads/test_messages.py | 10 ++++++++-- tests/api_resources/threads/test_runs.py | 20 +++++++++++++++---- 7 files changed, 36 insertions(+), 10 deletions(-) diff --git a/.stats.yml b/.stats.yml index 10d233f..b7535e3 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 42 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-043c2e8fdb798983c2f46d3e1a7bebcb6dcca5f0e92cf7ecfc9de02c6c27f55a.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-d53dc22c355880ee4966cfaa123a425658a66b0fe411b83beee37fa4a976cc45.yml diff --git a/src/agility/types/threads/message.py b/src/agility/types/threads/message.py index 297b74c..191c549 100644 --- a/src/agility/types/threads/message.py +++ b/src/agility/types/threads/message.py @@ -10,6 +10,8 @@ class Metadata(BaseModel): + citations: Optional[str] = None + trustworthiness_score: Optional[float] = None diff --git a/src/agility/types/threads/message_create_params.py b/src/agility/types/threads/message_create_params.py index d1ec82e..cccbb06 100644 --- a/src/agility/types/threads/message_create_params.py +++ b/src/agility/types/threads/message_create_params.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Optional +from typing import List, Optional from typing_extensions import Literal, Required, TypedDict __all__ = ["MessageCreateParams", "Metadata"] @@ -17,4 +17,6 @@ class MessageCreateParams(TypedDict, total=False): class Metadata(TypedDict, total=False): + citations: Optional[List[str]] + trustworthiness_score: Optional[float] diff --git a/src/agility/types/threads/run_create_params.py b/src/agility/types/threads/run_create_params.py index b42f8a7..fc67387 100644 --- a/src/agility/types/threads/run_create_params.py +++ b/src/agility/types/threads/run_create_params.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Iterable, Optional +from typing import List, Iterable, Optional from typing_extensions import Literal, Required, TypedDict __all__ = ["RunCreateParams", "AdditionalMessage", "AdditionalMessageMetadata"] @@ -23,6 +23,8 @@ class RunCreateParams(TypedDict, total=False): class AdditionalMessageMetadata(TypedDict, total=False): + citations: Optional[List[str]] + trustworthiness_score: Optional[float] diff --git a/src/agility/types/threads/run_stream_params.py b/src/agility/types/threads/run_stream_params.py index 804d30f..6198398 100644 --- a/src/agility/types/threads/run_stream_params.py +++ b/src/agility/types/threads/run_stream_params.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Iterable, Optional +from typing import List, Iterable, Optional from typing_extensions import Literal, Required, TypedDict __all__ = ["RunStreamParams", "AdditionalMessage", "AdditionalMessageMetadata"] @@ -23,6 +23,8 @@ class RunStreamParams(TypedDict, total=False): class AdditionalMessageMetadata(TypedDict, total=False): + citations: Optional[List[str]] + trustworthiness_score: Optional[float] diff --git a/tests/api_resources/threads/test_messages.py b/tests/api_resources/threads/test_messages.py index 1741837..6c7578b 100644 --- a/tests/api_resources/threads/test_messages.py +++ b/tests/api_resources/threads/test_messages.py @@ -33,7 +33,10 @@ def test_method_create_with_all_params(self, client: Agility) -> None: message = client.threads.messages.create( thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", content="content", - metadata={"trustworthiness_score": 0}, + metadata={ + "citations": ["string"], + "trustworthiness_score": 0, + }, role="user", ) assert_matches_type(Message, message, path=["response"]) @@ -240,7 +243,10 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgility) - message = await async_client.threads.messages.create( thread_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", content="content", - metadata={"trustworthiness_score": 0}, + metadata={ + "citations": ["string"], + "trustworthiness_score": 0, + }, role="user", ) assert_matches_type(Message, message, path=["response"]) diff --git a/tests/api_resources/threads/test_runs.py b/tests/api_resources/threads/test_runs.py index 55f611c..4ed1139 100644 --- a/tests/api_resources/threads/test_runs.py +++ b/tests/api_resources/threads/test_runs.py @@ -34,7 +34,10 @@ def test_method_create_with_all_params(self, client: Agility) -> None: additional_messages=[ { "content": "content", - "metadata": {"trustworthiness_score": 0}, + "metadata": { + "citations": ["string"], + "trustworthiness_score": 0, + }, "role": "user", "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", } @@ -192,7 +195,10 @@ def test_method_stream_with_all_params(self, client: Agility) -> None: additional_messages=[ { "content": "content", - "metadata": {"trustworthiness_score": 0}, + "metadata": { + "citations": ["string"], + "trustworthiness_score": 0, + }, "role": "user", "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", } @@ -258,7 +264,10 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgility) - additional_messages=[ { "content": "content", - "metadata": {"trustworthiness_score": 0}, + "metadata": { + "citations": ["string"], + "trustworthiness_score": 0, + }, "role": "user", "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", } @@ -416,7 +425,10 @@ async def test_method_stream_with_all_params(self, async_client: AsyncAgility) - additional_messages=[ { "content": "content", - "metadata": {"trustworthiness_score": 0}, + "metadata": { + "citations": ["string"], + "trustworthiness_score": 0, + }, "role": "user", "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", } From 4de0c000f67784396c7f15cb0836a155326fbf2e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 18:44:24 +0000 Subject: [PATCH 31/77] feat(api): api update --- .stats.yml | 2 +- src/agility/types/threads/message.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index b7535e3..0c3f540 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 42 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-d53dc22c355880ee4966cfaa123a425658a66b0fe411b83beee37fa4a976cc45.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-ba95a7c2fd2d8d1f314a907eca9f0f4f794f5f8c73bbf7389d9878edec34f53a.yml diff --git a/src/agility/types/threads/message.py b/src/agility/types/threads/message.py index 191c549..c5d56cf 100644 --- a/src/agility/types/threads/message.py +++ b/src/agility/types/threads/message.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional +from typing import List, Optional from datetime import datetime from typing_extensions import Literal @@ -10,7 +10,7 @@ class Metadata(BaseModel): - citations: Optional[str] = None + citations: Optional[List[str]] = None trustworthiness_score: Optional[float] = None From 1cd6248632cf5de5715f23005e483e60bdf68e85 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 13 Dec 2024 07:38:48 +0000 Subject: [PATCH 32/77] chore(internal): bump pyright --- requirements-dev.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.lock b/requirements-dev.lock index 8557979..77bf3cc 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 35d3d2d113a710611022aff9c4e47a4acd5fef69 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 13 Dec 2024 07:39:24 +0000 Subject: [PATCH 33/77] chore(internal): add support for TypeAliasType --- pyproject.toml | 2 +- src/agility/_models.py | 3 +++ src/agility/_response.py | 20 ++++++++++---------- src/agility/_utils/__init__.py | 1 + src/agility/_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 94cb9ac..906db9e 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/agility/_models.py b/src/agility/_models.py index 6cb469e..7a547ce 100644 --- a/src/agility/_models.py +++ b/src/agility/_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/agility/_response.py b/src/agility/_response.py index 3ac2a64..00f5b74 100644 --- a/src/agility/_response.py +++ b/src/agility/_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/agility/_utils/__init__.py b/src/agility/_utils/__init__.py index a7cff3c..d4fda26 100644 --- a/src/agility/_utils/__init__.py +++ b/src/agility/_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/agility/_utils/_typing.py b/src/agility/_utils/_typing.py index c036991..278749b 100644 --- a/src/agility/_utils/_typing.py +++ b/src/agility/_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 2b39471..a224db3 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 ecdcb91..e295690 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 agility._compat import PYDANTIC_V2, field_outer_type, get_model_fields from agility._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 5e34e65b8d0bd1790f934799ced8037fd59c21c5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 13 Dec 2024 20:24:29 +0000 Subject: [PATCH 34/77] feat(api): api update --- .stats.yml | 2 +- api.md | 4 +- .../resources/assistants/assistants.py | 16 ++ .../knowledge_bases/knowledge_bases.py | 13 +- src/agility/resources/threads/runs.py | 16 ++ src/agility/types/__init__.py | 1 + src/agility/types/assistant_create_params.py | 3 + src/agility/types/assistant_list_response.py | 3 + src/agility/types/assistant_update_params.py | 3 + src/agility/types/assistant_with_config.py | 3 + .../types/knowledge_base_create_params.py | 10 + .../types/knowledge_base_list_response.py | 199 ++++++++++++++++++ .../types/knowledge_base_update_params.py | 10 + .../types/knowledge_base_with_config.py | 10 + src/agility/types/knowledge_bases/source.py | 60 ++++++ src/agility/types/threads/run.py | 3 + .../types/threads/run_create_params.py | 3 + .../types/threads/run_stream_params.py | 3 + tests/api_resources/test_assistants.py | 4 + tests/api_resources/test_knowledge_bases.py | 17 +- tests/api_resources/threads/test_runs.py | 4 + 21 files changed, 370 insertions(+), 17 deletions(-) create mode 100644 src/agility/types/knowledge_base_list_response.py diff --git a/.stats.yml b/.stats.yml index 0c3f540..e8a0030 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 42 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-ba95a7c2fd2d8d1f314a907eca9f0f4f794f5f8c73bbf7389d9878edec34f53a.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-a5e85dccbbe3de5dd205ee7b7952430a69328e5ac7867e79f3ed638e2e818bc5.yml diff --git a/api.md b/api.md index a7a6def..7b79cef 100644 --- a/api.md +++ b/api.md @@ -32,7 +32,7 @@ Methods: Types: ```python -from agility.types import KnowledgeBaseWithConfig +from agility.types import KnowledgeBaseWithConfig, KnowledgeBaseListResponse ``` Methods: @@ -40,7 +40,7 @@ Methods: - client.knowledge_bases.create(\*\*params) -> KnowledgeBaseWithConfig - client.knowledge_bases.retrieve(knowledge_base_id) -> KnowledgeBaseWithConfig - client.knowledge_bases.update(knowledge_base_id, \*\*params) -> KnowledgeBaseWithConfig -- client.knowledge_bases.list(\*\*params) -> SyncMyOffsetPage[KnowledgeBaseWithConfig] +- client.knowledge_bases.list(\*\*params) -> SyncMyOffsetPage[KnowledgeBaseListResponse] - client.knowledge_bases.delete(knowledge_base_id) -> None ## Sources diff --git a/src/agility/resources/assistants/assistants.py b/src/agility/resources/assistants/assistants.py index f484f70..4acf958 100644 --- a/src/agility/resources/assistants/assistants.py +++ b/src/agility/resources/assistants/assistants.py @@ -68,6 +68,7 @@ def create( description: str, knowledge_base_id: Optional[str], name: str, + context_limit: Optional[int] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, suggested_questions: List[str] | NotGiven = NOT_GIVEN, @@ -87,6 +88,8 @@ def create( name: The name of the assistant + context_limit: The maximum number of context chunks to include in a run. + suggested_questions: A list of suggested questions that can be asked to the assistant url_slug: Optional URL suffix - unique identifier for the assistant's endpoint @@ -106,6 +109,7 @@ def create( "description": description, "knowledge_base_id": knowledge_base_id, "name": name, + "context_limit": context_limit, "instructions": instructions, "model": model, "suggested_questions": suggested_questions, @@ -160,6 +164,7 @@ def update( description: str, knowledge_base_id: Optional[str], name: str, + context_limit: Optional[int] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, suggested_questions: List[str] | NotGiven = NOT_GIVEN, @@ -179,6 +184,8 @@ def update( name: The name of the assistant + context_limit: The maximum number of context chunks to include in a run. + suggested_questions: A list of suggested questions that can be asked to the assistant url_slug: Optional URL suffix - unique identifier for the assistant's endpoint @@ -201,6 +208,7 @@ def update( "description": description, "knowledge_base_id": knowledge_base_id, "name": name, + "context_limit": context_limit, "instructions": instructions, "model": model, "suggested_questions": suggested_questions, @@ -322,6 +330,7 @@ async def create( description: str, knowledge_base_id: Optional[str], name: str, + context_limit: Optional[int] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, suggested_questions: List[str] | NotGiven = NOT_GIVEN, @@ -341,6 +350,8 @@ async def create( name: The name of the assistant + context_limit: The maximum number of context chunks to include in a run. + suggested_questions: A list of suggested questions that can be asked to the assistant url_slug: Optional URL suffix - unique identifier for the assistant's endpoint @@ -360,6 +371,7 @@ async def create( "description": description, "knowledge_base_id": knowledge_base_id, "name": name, + "context_limit": context_limit, "instructions": instructions, "model": model, "suggested_questions": suggested_questions, @@ -414,6 +426,7 @@ async def update( description: str, knowledge_base_id: Optional[str], name: str, + context_limit: Optional[int] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, suggested_questions: List[str] | NotGiven = NOT_GIVEN, @@ -433,6 +446,8 @@ async def update( name: The name of the assistant + context_limit: The maximum number of context chunks to include in a run. + suggested_questions: A list of suggested questions that can be asked to the assistant url_slug: Optional URL suffix - unique identifier for the assistant's endpoint @@ -455,6 +470,7 @@ async def update( "description": description, "knowledge_base_id": knowledge_base_id, "name": name, + "context_limit": context_limit, "instructions": instructions, "model": model, "suggested_questions": suggested_questions, diff --git a/src/agility/resources/knowledge_bases/knowledge_bases.py b/src/agility/resources/knowledge_bases/knowledge_bases.py index c55b657..807d159 100644 --- a/src/agility/resources/knowledge_bases/knowledge_bases.py +++ b/src/agility/resources/knowledge_bases/knowledge_bases.py @@ -34,6 +34,7 @@ from ..._base_client import AsyncPaginator, make_request_options from .sources.sources import SourcesResource, AsyncSourcesResource from ...types.knowledge_base_with_config import KnowledgeBaseWithConfig +from ...types.knowledge_base_list_response import KnowledgeBaseListResponse __all__ = ["KnowledgeBasesResource", "AsyncKnowledgeBasesResource"] @@ -199,7 +200,7 @@ def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SyncMyOffsetPage[KnowledgeBaseWithConfig]: + ) -> SyncMyOffsetPage[KnowledgeBaseListResponse]: """ List all knowledge bases. @@ -214,7 +215,7 @@ def list( """ return self._get_api_list( "/api/knowledge_bases/", - page=SyncMyOffsetPage[KnowledgeBaseWithConfig], + page=SyncMyOffsetPage[KnowledgeBaseListResponse], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -228,7 +229,7 @@ def list( knowledge_base_list_params.KnowledgeBaseListParams, ), ), - model=KnowledgeBaseWithConfig, + model=KnowledgeBaseListResponse, ) def delete( @@ -427,7 +428,7 @@ def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AsyncPaginator[KnowledgeBaseWithConfig, AsyncMyOffsetPage[KnowledgeBaseWithConfig]]: + ) -> AsyncPaginator[KnowledgeBaseListResponse, AsyncMyOffsetPage[KnowledgeBaseListResponse]]: """ List all knowledge bases. @@ -442,7 +443,7 @@ def list( """ return self._get_api_list( "/api/knowledge_bases/", - page=AsyncMyOffsetPage[KnowledgeBaseWithConfig], + page=AsyncMyOffsetPage[KnowledgeBaseListResponse], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -456,7 +457,7 @@ def list( knowledge_base_list_params.KnowledgeBaseListParams, ), ), - model=KnowledgeBaseWithConfig, + model=KnowledgeBaseListResponse, ) async def delete( diff --git a/src/agility/resources/threads/runs.py b/src/agility/resources/threads/runs.py index f76a5f7..ae3431e 100644 --- a/src/agility/resources/threads/runs.py +++ b/src/agility/resources/threads/runs.py @@ -54,6 +54,7 @@ def create( assistant_id: str, additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, additional_messages: Iterable[run_create_params.AdditionalMessage] | NotGiven = NOT_GIVEN, + context_limit: Optional[int] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, knowledge_base_id: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, @@ -68,6 +69,8 @@ def create( Creates a new run, starting it in the background. Args: + context_limit: The maximum number of context chunks to include. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -85,6 +88,7 @@ def create( "assistant_id": assistant_id, "additional_instructions": additional_instructions, "additional_messages": additional_messages, + "context_limit": context_limit, "instructions": instructions, "knowledge_base_id": knowledge_base_id, "model": model, @@ -177,6 +181,7 @@ def stream( assistant_id: str, additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, additional_messages: Iterable[run_stream_params.AdditionalMessage] | NotGiven = NOT_GIVEN, + context_limit: Optional[int] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, knowledge_base_id: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, @@ -191,6 +196,8 @@ def stream( Creates a new run and streams the results. Args: + context_limit: The maximum number of context chunks to include. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -208,6 +215,7 @@ def stream( "assistant_id": assistant_id, "additional_instructions": additional_instructions, "additional_messages": additional_messages, + "context_limit": context_limit, "instructions": instructions, "knowledge_base_id": knowledge_base_id, "model": model, @@ -248,6 +256,7 @@ async def create( assistant_id: str, additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, additional_messages: Iterable[run_create_params.AdditionalMessage] | NotGiven = NOT_GIVEN, + context_limit: Optional[int] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, knowledge_base_id: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, @@ -262,6 +271,8 @@ async def create( Creates a new run, starting it in the background. Args: + context_limit: The maximum number of context chunks to include. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -279,6 +290,7 @@ async def create( "assistant_id": assistant_id, "additional_instructions": additional_instructions, "additional_messages": additional_messages, + "context_limit": context_limit, "instructions": instructions, "knowledge_base_id": knowledge_base_id, "model": model, @@ -371,6 +383,7 @@ async def stream( assistant_id: str, additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, additional_messages: Iterable[run_stream_params.AdditionalMessage] | NotGiven = NOT_GIVEN, + context_limit: Optional[int] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, knowledge_base_id: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, @@ -385,6 +398,8 @@ async def stream( Creates a new run and streams the results. Args: + context_limit: The maximum number of context chunks to include. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -402,6 +417,7 @@ async def stream( "assistant_id": assistant_id, "additional_instructions": additional_instructions, "additional_messages": additional_messages, + "context_limit": context_limit, "instructions": instructions, "knowledge_base_id": knowledge_base_id, "model": model, diff --git a/src/agility/types/__init__.py b/src/agility/types/__init__.py index 29bd181..a9380ef 100644 --- a/src/agility/types/__init__.py +++ b/src/agility/types/__init__.py @@ -21,5 +21,6 @@ from .knowledge_base_with_config import KnowledgeBaseWithConfig as KnowledgeBaseWithConfig from .integration_create_response import IntegrationCreateResponse as IntegrationCreateResponse from .knowledge_base_create_params import KnowledgeBaseCreateParams as KnowledgeBaseCreateParams +from .knowledge_base_list_response import KnowledgeBaseListResponse as KnowledgeBaseListResponse from .knowledge_base_update_params import KnowledgeBaseUpdateParams as KnowledgeBaseUpdateParams from .integration_retrieve_response import IntegrationRetrieveResponse as IntegrationRetrieveResponse diff --git a/src/agility/types/assistant_create_params.py b/src/agility/types/assistant_create_params.py index 5e694da..301b279 100644 --- a/src/agility/types/assistant_create_params.py +++ b/src/agility/types/assistant_create_params.py @@ -17,6 +17,9 @@ class AssistantCreateParams(TypedDict, total=False): name: Required[str] """The name of the assistant""" + context_limit: Optional[int] + """The maximum number of context chunks to include in a run.""" + instructions: Optional[str] model: Optional[Literal["gpt-4o"]] diff --git a/src/agility/types/assistant_list_response.py b/src/agility/types/assistant_list_response.py index 0d81886..1119326 100644 --- a/src/agility/types/assistant_list_response.py +++ b/src/agility/types/assistant_list_response.py @@ -28,6 +28,9 @@ class AssistantListResponse(BaseModel): updated_at: datetime + context_limit: Optional[int] = None + """The maximum number of context chunks to include in a run.""" + instructions: Optional[str] = None model: Optional[Literal["gpt-4o"]] = None diff --git a/src/agility/types/assistant_update_params.py b/src/agility/types/assistant_update_params.py index 3208db8..329a598 100644 --- a/src/agility/types/assistant_update_params.py +++ b/src/agility/types/assistant_update_params.py @@ -19,6 +19,9 @@ class AssistantUpdateParams(TypedDict, total=False): name: Required[str] """The name of the assistant""" + context_limit: Optional[int] + """The maximum number of context chunks to include in a run.""" + instructions: Optional[str] model: Optional[Literal["gpt-4o"]] diff --git a/src/agility/types/assistant_with_config.py b/src/agility/types/assistant_with_config.py index d9aa530..e3e1561 100644 --- a/src/agility/types/assistant_with_config.py +++ b/src/agility/types/assistant_with_config.py @@ -26,6 +26,9 @@ class AssistantWithConfig(BaseModel): updated_at: datetime + context_limit: Optional[int] = None + """The maximum number of context chunks to include in a run.""" + instructions: Optional[str] = None model: Optional[Literal["gpt-4o"]] = None diff --git a/src/agility/types/knowledge_base_create_params.py b/src/agility/types/knowledge_base_create_params.py index 2ce6be4..36e547e 100644 --- a/src/agility/types/knowledge_base_create_params.py +++ b/src/agility/types/knowledge_base_create_params.py @@ -18,6 +18,7 @@ "IngestionPipelineParamsTransform", "IngestionPipelineParamsTransformSteps", "IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params", + "IngestionPipelineParamsTransformStepsCharacterSplitterV0Params", "IngestionPipelineParamsTransformStepsSemanticMergeSplitterV0Params", "IngestionPipelineParamsTransformStepsMarkdownNodeExpanderParams", "IngestionPipelineParamsTransformStepsNodeSummarizerV0Params", @@ -81,6 +82,14 @@ class IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params(Ty name: Literal["splitters.recursive_character.v0"] +class IngestionPipelineParamsTransformStepsCharacterSplitterV0Params(TypedDict, total=False): + chunk_overlap: int + + chunk_size: int + + name: Literal["splitters.character.v0"] + + class IngestionPipelineParamsTransformStepsSemanticMergeSplitterV0Params(TypedDict, total=False): appending_threshold: float @@ -130,6 +139,7 @@ class IngestionPipelineParamsTransformStepsNoopParams(TypedDict, total=False): IngestionPipelineParamsTransformSteps: TypeAlias = Union[ IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params, + IngestionPipelineParamsTransformStepsCharacterSplitterV0Params, IngestionPipelineParamsTransformStepsSemanticMergeSplitterV0Params, IngestionPipelineParamsTransformStepsMarkdownNodeExpanderParams, IngestionPipelineParamsTransformStepsNodeSummarizerV0Params, diff --git a/src/agility/types/knowledge_base_list_response.py b/src/agility/types/knowledge_base_list_response.py new file mode 100644 index 0000000..8454f48 --- /dev/null +++ b/src/agility/types/knowledge_base_list_response.py @@ -0,0 +1,199 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, Union, Optional +from datetime import datetime +from typing_extensions import Literal, Annotated, TypeAlias + +from .._utils import PropertyInfo +from .._models import BaseModel + +__all__ = [ + "KnowledgeBaseListResponse", + "IngestionPipelineParams", + "IngestionPipelineParamsCurate", + "IngestionPipelineParamsCurateSteps", + "IngestionPipelineParamsCurateStepsRemoveExactDuplicatesParams", + "IngestionPipelineParamsCurateStepsTagExactDuplicatesParams", + "IngestionPipelineParamsCurateStepsPostpendContentParams", + "IngestionPipelineParamsCurateStepsRemoveEmbeddedImagesParams", + "IngestionPipelineParamsCurateDocumentStore", + "IngestionPipelineParamsTransform", + "IngestionPipelineParamsTransformSteps", + "IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params", + "IngestionPipelineParamsTransformStepsCharacterSplitterV0Params", + "IngestionPipelineParamsTransformStepsSemanticMergeSplitterV0Params", + "IngestionPipelineParamsTransformStepsMarkdownNodeExpanderParams", + "IngestionPipelineParamsTransformStepsNodeSummarizerV0Params", + "IngestionPipelineParamsTransformStepsNoopParams", + "IngestionPipelineParamsVectorStore", +] + + +class IngestionPipelineParamsCurateStepsRemoveExactDuplicatesParams(BaseModel): + name: Optional[Literal["remove_exact_duplicates.v0"]] = None + + +class IngestionPipelineParamsCurateStepsTagExactDuplicatesParams(BaseModel): + name: Optional[Literal["tag_exact_duplicates.v0"]] = None + + +class IngestionPipelineParamsCurateStepsPostpendContentParams(BaseModel): + postpend_value: str + """The value to postpend to the content.""" + + name: Optional[Literal["postpend_content.v0"]] = None + + +class IngestionPipelineParamsCurateStepsRemoveEmbeddedImagesParams(BaseModel): + name: Optional[Literal["remove_embedded_images.v0"]] = None + + +IngestionPipelineParamsCurateSteps: TypeAlias = Annotated[ + Union[ + IngestionPipelineParamsCurateStepsRemoveExactDuplicatesParams, + IngestionPipelineParamsCurateStepsTagExactDuplicatesParams, + IngestionPipelineParamsCurateStepsPostpendContentParams, + IngestionPipelineParamsCurateStepsRemoveEmbeddedImagesParams, + ], + PropertyInfo(discriminator="name"), +] + + +class IngestionPipelineParamsCurate(BaseModel): + steps: Optional[Dict[str, IngestionPipelineParamsCurateSteps]] = None + + +class IngestionPipelineParamsCurateDocumentStore(BaseModel): + document_tags: Optional[Dict[str, str]] = None + + +class IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params(BaseModel): + chunk_overlap: Optional[int] = None + + chunk_size: Optional[int] = None + + name: Optional[Literal["splitters.recursive_character.v0"]] = None + + +class IngestionPipelineParamsTransformStepsCharacterSplitterV0Params(BaseModel): + chunk_overlap: Optional[int] = None + + chunk_size: Optional[int] = None + + name: Optional[Literal["splitters.character.v0"]] = None + + +class IngestionPipelineParamsTransformStepsSemanticMergeSplitterV0Params(BaseModel): + appending_threshold: Optional[float] = None + + initial_threshold: Optional[float] = None + + max_chunk_size: Optional[int] = None + + merging_range: Optional[int] = None + + merging_threshold: Optional[float] = None + + name: Optional[Literal["splitters.semantic_merge.v0"]] = None + + +class IngestionPipelineParamsTransformStepsMarkdownNodeExpanderParams(BaseModel): + code_block_pattern: Optional[str] = None + """A regex pattern used to identify code blocks in markdown. + + Matches both multi-line code blocks enclosed in triple backticks and inline code + wrapped in single backticks. + """ + + name: Optional[Literal["node_expander.v0"]] = None + """The version identifier for the node expander.""" + + section_delimiter_pattern: Optional[str] = None + """A regex pattern used to identify markdown sections. + + Matches headers of level 1 to 6, capturing the section title and content until + the next header. + """ + + +class IngestionPipelineParamsTransformStepsNodeSummarizerV0Params(BaseModel): + expected_summary_tokens: Optional[int] = None + + max_prompt_input_tokens: Optional[int] = None + + model: Optional[str] = None + + name: Optional[Literal["node_summarizer.v0"]] = None + + +class IngestionPipelineParamsTransformStepsNoopParams(BaseModel): + name: Optional[Literal["noop"]] = None + + +IngestionPipelineParamsTransformSteps: TypeAlias = Union[ + IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params, + IngestionPipelineParamsTransformStepsCharacterSplitterV0Params, + IngestionPipelineParamsTransformStepsSemanticMergeSplitterV0Params, + IngestionPipelineParamsTransformStepsMarkdownNodeExpanderParams, + IngestionPipelineParamsTransformStepsNodeSummarizerV0Params, + IngestionPipelineParamsTransformStepsNoopParams, +] + + +class IngestionPipelineParamsTransform(BaseModel): + steps: Optional[Dict[str, IngestionPipelineParamsTransformSteps]] = None + + +class IngestionPipelineParamsVectorStore(BaseModel): + weaviate_collection_name: str + """The name of the Weaviate collection to use for storing documents. + + Must start with AgilityKB and be valid. + """ + + node_tags: Optional[Dict[str, str]] = None + + +class IngestionPipelineParams(BaseModel): + curate: IngestionPipelineParamsCurate + """Curate params. + + Defines full curation pipeline, as an ordered dict of named curation steps. + Order of steps _does_ matter -- they are executed in the order defined. + """ + + curate_document_store: IngestionPipelineParamsCurateDocumentStore + """Document store params.""" + + transform: IngestionPipelineParamsTransform + """Transform params. + + Defines full transform pipeline, as an ordered dict of named transform steps. + Order of steps _does_ matter -- they are executed in the order defined. + """ + + vector_store: IngestionPipelineParamsVectorStore + """Vector store params.""" + + +class KnowledgeBaseListResponse(BaseModel): + id: str + + created_at: datetime + + deleted_at: Optional[datetime] = None + + description: str + + ingestion_pipeline_params: IngestionPipelineParams + """Knowledge base pipeline params. + + Parameters defined on the knowledge-base level for a pipeline. + """ + + name: str + + status: Literal["pending", "syncing", "synced", "failed"] + """Source status enum.""" + + updated_at: datetime diff --git a/src/agility/types/knowledge_base_update_params.py b/src/agility/types/knowledge_base_update_params.py index 3c899d2..c5ba671 100644 --- a/src/agility/types/knowledge_base_update_params.py +++ b/src/agility/types/knowledge_base_update_params.py @@ -18,6 +18,7 @@ "IngestionPipelineParamsTransform", "IngestionPipelineParamsTransformSteps", "IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params", + "IngestionPipelineParamsTransformStepsCharacterSplitterV0Params", "IngestionPipelineParamsTransformStepsSemanticMergeSplitterV0Params", "IngestionPipelineParamsTransformStepsMarkdownNodeExpanderParams", "IngestionPipelineParamsTransformStepsNodeSummarizerV0Params", @@ -81,6 +82,14 @@ class IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params(Ty name: Literal["splitters.recursive_character.v0"] +class IngestionPipelineParamsTransformStepsCharacterSplitterV0Params(TypedDict, total=False): + chunk_overlap: int + + chunk_size: int + + name: Literal["splitters.character.v0"] + + class IngestionPipelineParamsTransformStepsSemanticMergeSplitterV0Params(TypedDict, total=False): appending_threshold: float @@ -130,6 +139,7 @@ class IngestionPipelineParamsTransformStepsNoopParams(TypedDict, total=False): IngestionPipelineParamsTransformSteps: TypeAlias = Union[ IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params, + IngestionPipelineParamsTransformStepsCharacterSplitterV0Params, IngestionPipelineParamsTransformStepsSemanticMergeSplitterV0Params, IngestionPipelineParamsTransformStepsMarkdownNodeExpanderParams, IngestionPipelineParamsTransformStepsNodeSummarizerV0Params, diff --git a/src/agility/types/knowledge_base_with_config.py b/src/agility/types/knowledge_base_with_config.py index 61365c7..8a5984b 100644 --- a/src/agility/types/knowledge_base_with_config.py +++ b/src/agility/types/knowledge_base_with_config.py @@ -20,6 +20,7 @@ "IngestionPipelineParamsTransform", "IngestionPipelineParamsTransformSteps", "IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params", + "IngestionPipelineParamsTransformStepsCharacterSplitterV0Params", "IngestionPipelineParamsTransformStepsSemanticMergeSplitterV0Params", "IngestionPipelineParamsTransformStepsMarkdownNodeExpanderParams", "IngestionPipelineParamsTransformStepsNodeSummarizerV0Params", @@ -74,6 +75,14 @@ class IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params(Ba name: Optional[Literal["splitters.recursive_character.v0"]] = None +class IngestionPipelineParamsTransformStepsCharacterSplitterV0Params(BaseModel): + chunk_overlap: Optional[int] = None + + chunk_size: Optional[int] = None + + name: Optional[Literal["splitters.character.v0"]] = None + + class IngestionPipelineParamsTransformStepsSemanticMergeSplitterV0Params(BaseModel): appending_threshold: Optional[float] = None @@ -123,6 +132,7 @@ class IngestionPipelineParamsTransformStepsNoopParams(BaseModel): IngestionPipelineParamsTransformSteps: TypeAlias = Union[ IngestionPipelineParamsTransformStepsRecursiveCharacterSplitterV0Params, + IngestionPipelineParamsTransformStepsCharacterSplitterV0Params, IngestionPipelineParamsTransformStepsSemanticMergeSplitterV0Params, IngestionPipelineParamsTransformStepsMarkdownNodeExpanderParams, IngestionPipelineParamsTransformStepsNodeSummarizerV0Params, diff --git a/src/agility/types/knowledge_bases/source.py b/src/agility/types/knowledge_bases/source.py index 7050d5d..77bb40e 100644 --- a/src/agility/types/knowledge_bases/source.py +++ b/src/agility/types/knowledge_bases/source.py @@ -16,6 +16,11 @@ "SourceParamsS3PublicV0Params", "SourceParamsS3PrivateV0Params", "SourceSchedule", + "Progress", + "ProgressComplete", + "ProgressCurate", + "ProgressLoad", + "ProgressTransform", ] @@ -114,6 +119,58 @@ class SourceSchedule(BaseModel): utc_offset: int +class ProgressComplete(BaseModel): + processed_documents: Optional[int] = None + + processed_nodes: Optional[int] = None + + result_documents: Optional[int] = None + + result_nodes: Optional[int] = None + + +class ProgressCurate(BaseModel): + processed_documents: Optional[int] = None + + processed_nodes: Optional[int] = None + + result_documents: Optional[int] = None + + result_nodes: Optional[int] = None + + +class ProgressLoad(BaseModel): + processed_documents: Optional[int] = None + + processed_nodes: Optional[int] = None + + result_documents: Optional[int] = None + + result_nodes: Optional[int] = None + + +class ProgressTransform(BaseModel): + processed_documents: Optional[int] = None + + processed_nodes: Optional[int] = None + + result_documents: Optional[int] = None + + result_nodes: Optional[int] = None + + +class Progress(BaseModel): + complete: Optional[ProgressComplete] = None + """Step progress model.""" + + curate: Optional[Dict[str, ProgressCurate]] = None + + load: Optional[ProgressLoad] = None + """Step progress model.""" + + transform: Optional[Dict[str, ProgressTransform]] = None + + class Source(BaseModel): id: str @@ -137,3 +194,6 @@ class Source(BaseModel): """Source status enum.""" updated_at: datetime + + progress: Optional[Progress] = None + """Source progress model.""" diff --git a/src/agility/types/threads/run.py b/src/agility/types/threads/run.py index a8ef291..cbb8f45 100644 --- a/src/agility/types/threads/run.py +++ b/src/agility/types/threads/run.py @@ -32,6 +32,9 @@ class Run(BaseModel): additional_instructions: Optional[str] = None + context_limit: Optional[int] = None + """The maximum number of context chunks to include.""" + deleted_at: Optional[datetime] = None instructions: Optional[str] = None diff --git a/src/agility/types/threads/run_create_params.py b/src/agility/types/threads/run_create_params.py index fc67387..a8b1082 100644 --- a/src/agility/types/threads/run_create_params.py +++ b/src/agility/types/threads/run_create_params.py @@ -15,6 +15,9 @@ class RunCreateParams(TypedDict, total=False): additional_messages: Iterable[AdditionalMessage] + context_limit: Optional[int] + """The maximum number of context chunks to include.""" + instructions: Optional[str] knowledge_base_id: Optional[str] diff --git a/src/agility/types/threads/run_stream_params.py b/src/agility/types/threads/run_stream_params.py index 6198398..dd96db3 100644 --- a/src/agility/types/threads/run_stream_params.py +++ b/src/agility/types/threads/run_stream_params.py @@ -15,6 +15,9 @@ class RunStreamParams(TypedDict, total=False): additional_messages: Iterable[AdditionalMessage] + context_limit: Optional[int] + """The maximum number of context chunks to include.""" + instructions: Optional[str] knowledge_base_id: Optional[str] diff --git a/tests/api_resources/test_assistants.py b/tests/api_resources/test_assistants.py index f4d2871..80a5916 100644 --- a/tests/api_resources/test_assistants.py +++ b/tests/api_resources/test_assistants.py @@ -37,6 +37,7 @@ def test_method_create_with_all_params(self, client: Agility) -> None: description="description", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", name="name", + context_limit=1, instructions="instructions", model="gpt-4o", suggested_questions=["string"], @@ -129,6 +130,7 @@ def test_method_update_with_all_params(self, client: Agility) -> None: description="description", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", name="name", + context_limit=1, instructions="instructions", model="gpt-4o", suggested_questions=["string"], @@ -269,6 +271,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgility) - description="description", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", name="name", + context_limit=1, instructions="instructions", model="gpt-4o", suggested_questions=["string"], @@ -361,6 +364,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncAgility) - description="description", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", name="name", + context_limit=1, instructions="instructions", model="gpt-4o", suggested_questions=["string"], diff --git a/tests/api_resources/test_knowledge_bases.py b/tests/api_resources/test_knowledge_bases.py index 34a0eca..2e915ba 100644 --- a/tests/api_resources/test_knowledge_bases.py +++ b/tests/api_resources/test_knowledge_bases.py @@ -11,6 +11,7 @@ from tests.utils import assert_matches_type from agility.types import ( KnowledgeBaseWithConfig, + KnowledgeBaseListResponse, ) from agility.pagination import SyncMyOffsetPage, AsyncMyOffsetPage @@ -183,7 +184,7 @@ def test_path_params_update(self, client: Agility) -> None: @parametrize def test_method_list(self, client: Agility) -> None: knowledge_base = client.knowledge_bases.list() - assert_matches_type(SyncMyOffsetPage[KnowledgeBaseWithConfig], knowledge_base, path=["response"]) + assert_matches_type(SyncMyOffsetPage[KnowledgeBaseListResponse], knowledge_base, path=["response"]) @parametrize def test_method_list_with_all_params(self, client: Agility) -> None: @@ -191,7 +192,7 @@ def test_method_list_with_all_params(self, client: Agility) -> None: limit=1, offset=0, ) - assert_matches_type(SyncMyOffsetPage[KnowledgeBaseWithConfig], knowledge_base, path=["response"]) + assert_matches_type(SyncMyOffsetPage[KnowledgeBaseListResponse], knowledge_base, path=["response"]) @parametrize def test_raw_response_list(self, client: Agility) -> None: @@ -200,7 +201,7 @@ def test_raw_response_list(self, client: Agility) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" knowledge_base = response.parse() - assert_matches_type(SyncMyOffsetPage[KnowledgeBaseWithConfig], knowledge_base, path=["response"]) + assert_matches_type(SyncMyOffsetPage[KnowledgeBaseListResponse], knowledge_base, path=["response"]) @parametrize def test_streaming_response_list(self, client: Agility) -> None: @@ -209,7 +210,7 @@ def test_streaming_response_list(self, client: Agility) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" knowledge_base = response.parse() - assert_matches_type(SyncMyOffsetPage[KnowledgeBaseWithConfig], knowledge_base, path=["response"]) + assert_matches_type(SyncMyOffsetPage[KnowledgeBaseListResponse], knowledge_base, path=["response"]) assert cast(Any, response.is_closed) is True @@ -418,7 +419,7 @@ async def test_path_params_update(self, async_client: AsyncAgility) -> None: @parametrize async def test_method_list(self, async_client: AsyncAgility) -> None: knowledge_base = await async_client.knowledge_bases.list() - assert_matches_type(AsyncMyOffsetPage[KnowledgeBaseWithConfig], knowledge_base, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[KnowledgeBaseListResponse], knowledge_base, path=["response"]) @parametrize async def test_method_list_with_all_params(self, async_client: AsyncAgility) -> None: @@ -426,7 +427,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncAgility) -> limit=1, offset=0, ) - assert_matches_type(AsyncMyOffsetPage[KnowledgeBaseWithConfig], knowledge_base, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[KnowledgeBaseListResponse], knowledge_base, path=["response"]) @parametrize async def test_raw_response_list(self, async_client: AsyncAgility) -> None: @@ -435,7 +436,7 @@ async def test_raw_response_list(self, async_client: AsyncAgility) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" knowledge_base = await response.parse() - assert_matches_type(AsyncMyOffsetPage[KnowledgeBaseWithConfig], knowledge_base, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[KnowledgeBaseListResponse], knowledge_base, path=["response"]) @parametrize async def test_streaming_response_list(self, async_client: AsyncAgility) -> None: @@ -444,7 +445,7 @@ async def test_streaming_response_list(self, async_client: AsyncAgility) -> None assert response.http_request.headers.get("X-Stainless-Lang") == "python" knowledge_base = await response.parse() - assert_matches_type(AsyncMyOffsetPage[KnowledgeBaseWithConfig], knowledge_base, path=["response"]) + assert_matches_type(AsyncMyOffsetPage[KnowledgeBaseListResponse], knowledge_base, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/threads/test_runs.py b/tests/api_resources/threads/test_runs.py index 4ed1139..5b509e3 100644 --- a/tests/api_resources/threads/test_runs.py +++ b/tests/api_resources/threads/test_runs.py @@ -42,6 +42,7 @@ def test_method_create_with_all_params(self, client: Agility) -> None: "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", } ], + context_limit=1, instructions="instructions", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", model="gpt-4o", @@ -203,6 +204,7 @@ def test_method_stream_with_all_params(self, client: Agility) -> None: "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", } ], + context_limit=1, instructions="instructions", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", model="gpt-4o", @@ -272,6 +274,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgility) - "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", } ], + context_limit=1, instructions="instructions", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", model="gpt-4o", @@ -433,6 +436,7 @@ async def test_method_stream_with_all_params(self, async_client: AsyncAgility) - "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", } ], + context_limit=1, instructions="instructions", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", model="gpt-4o", From 332241142ba0c80fe113537b4c50d7779e4a7313 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Dec 2024 07:35:23 +0000 Subject: [PATCH 35/77] chore(internal): codegen related update --- src/agility/_client.py | 88 ++++++++++--------- .../knowledge_bases/knowledge_bases.py | 17 ++-- 2 files changed, 54 insertions(+), 51 deletions(-) diff --git a/src/agility/_client.py b/src/agility/_client.py index a7dbd56..9fe8b29 100644 --- a/src/agility/_client.py +++ b/src/agility/_client.py @@ -8,7 +8,7 @@ import httpx -from . import resources, _exceptions +from . import _exceptions from ._qs import Querystring from ._types import ( NOT_GIVEN, @@ -31,6 +31,11 @@ SyncAPIClient, AsyncAPIClient, ) +from .resources.users import users +from .resources.threads import threads +from .resources.assistants import assistants +from .resources.integrations import integrations +from .resources.knowledge_bases import knowledge_bases __all__ = [ "ENVIRONMENTS", @@ -38,7 +43,6 @@ "Transport", "ProxiesTypes", "RequestOptions", - "resources", "Agility", "AsyncAgility", "Client", @@ -54,11 +58,11 @@ class Agility(SyncAPIClient): - assistants: resources.AssistantsResource - knowledge_bases: resources.KnowledgeBasesResource - users: resources.UsersResource - threads: resources.ThreadsResource - integrations: resources.IntegrationsResource + assistants: assistants.AssistantsResource + knowledge_bases: knowledge_bases.KnowledgeBasesResource + users: users.UsersResource + threads: threads.ThreadsResource + integrations: integrations.IntegrationsResource with_raw_response: AgilityWithRawResponse with_streaming_response: AgilityWithStreamedResponse @@ -155,11 +159,11 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.assistants = resources.AssistantsResource(self) - self.knowledge_bases = resources.KnowledgeBasesResource(self) - self.users = resources.UsersResource(self) - self.threads = resources.ThreadsResource(self) - self.integrations = resources.IntegrationsResource(self) + self.assistants = assistants.AssistantsResource(self) + self.knowledge_bases = knowledge_bases.KnowledgeBasesResource(self) + self.users = users.UsersResource(self) + self.threads = threads.ThreadsResource(self) + self.integrations = integrations.IntegrationsResource(self) self.with_raw_response = AgilityWithRawResponse(self) self.with_streaming_response = AgilityWithStreamedResponse(self) @@ -299,11 +303,11 @@ def _make_status_error( class AsyncAgility(AsyncAPIClient): - assistants: resources.AsyncAssistantsResource - knowledge_bases: resources.AsyncKnowledgeBasesResource - users: resources.AsyncUsersResource - threads: resources.AsyncThreadsResource - integrations: resources.AsyncIntegrationsResource + assistants: assistants.AsyncAssistantsResource + knowledge_bases: knowledge_bases.AsyncKnowledgeBasesResource + users: users.AsyncUsersResource + threads: threads.AsyncThreadsResource + integrations: integrations.AsyncIntegrationsResource with_raw_response: AsyncAgilityWithRawResponse with_streaming_response: AsyncAgilityWithStreamedResponse @@ -400,11 +404,11 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.assistants = resources.AsyncAssistantsResource(self) - self.knowledge_bases = resources.AsyncKnowledgeBasesResource(self) - self.users = resources.AsyncUsersResource(self) - self.threads = resources.AsyncThreadsResource(self) - self.integrations = resources.AsyncIntegrationsResource(self) + self.assistants = assistants.AsyncAssistantsResource(self) + self.knowledge_bases = knowledge_bases.AsyncKnowledgeBasesResource(self) + self.users = users.AsyncUsersResource(self) + self.threads = threads.AsyncThreadsResource(self) + self.integrations = integrations.AsyncIntegrationsResource(self) self.with_raw_response = AsyncAgilityWithRawResponse(self) self.with_streaming_response = AsyncAgilityWithStreamedResponse(self) @@ -545,38 +549,38 @@ def _make_status_error( class AgilityWithRawResponse: def __init__(self, client: Agility) -> None: - self.assistants = resources.AssistantsResourceWithRawResponse(client.assistants) - self.knowledge_bases = resources.KnowledgeBasesResourceWithRawResponse(client.knowledge_bases) - self.users = resources.UsersResourceWithRawResponse(client.users) - self.threads = resources.ThreadsResourceWithRawResponse(client.threads) - self.integrations = resources.IntegrationsResourceWithRawResponse(client.integrations) + self.assistants = assistants.AssistantsResourceWithRawResponse(client.assistants) + self.knowledge_bases = knowledge_bases.KnowledgeBasesResourceWithRawResponse(client.knowledge_bases) + self.users = users.UsersResourceWithRawResponse(client.users) + self.threads = threads.ThreadsResourceWithRawResponse(client.threads) + self.integrations = integrations.IntegrationsResourceWithRawResponse(client.integrations) class AsyncAgilityWithRawResponse: def __init__(self, client: AsyncAgility) -> None: - self.assistants = resources.AsyncAssistantsResourceWithRawResponse(client.assistants) - self.knowledge_bases = resources.AsyncKnowledgeBasesResourceWithRawResponse(client.knowledge_bases) - self.users = resources.AsyncUsersResourceWithRawResponse(client.users) - self.threads = resources.AsyncThreadsResourceWithRawResponse(client.threads) - self.integrations = resources.AsyncIntegrationsResourceWithRawResponse(client.integrations) + self.assistants = assistants.AsyncAssistantsResourceWithRawResponse(client.assistants) + self.knowledge_bases = knowledge_bases.AsyncKnowledgeBasesResourceWithRawResponse(client.knowledge_bases) + self.users = users.AsyncUsersResourceWithRawResponse(client.users) + self.threads = threads.AsyncThreadsResourceWithRawResponse(client.threads) + self.integrations = integrations.AsyncIntegrationsResourceWithRawResponse(client.integrations) class AgilityWithStreamedResponse: def __init__(self, client: Agility) -> None: - self.assistants = resources.AssistantsResourceWithStreamingResponse(client.assistants) - self.knowledge_bases = resources.KnowledgeBasesResourceWithStreamingResponse(client.knowledge_bases) - self.users = resources.UsersResourceWithStreamingResponse(client.users) - self.threads = resources.ThreadsResourceWithStreamingResponse(client.threads) - self.integrations = resources.IntegrationsResourceWithStreamingResponse(client.integrations) + self.assistants = assistants.AssistantsResourceWithStreamingResponse(client.assistants) + self.knowledge_bases = knowledge_bases.KnowledgeBasesResourceWithStreamingResponse(client.knowledge_bases) + self.users = users.UsersResourceWithStreamingResponse(client.users) + self.threads = threads.ThreadsResourceWithStreamingResponse(client.threads) + self.integrations = integrations.IntegrationsResourceWithStreamingResponse(client.integrations) class AsyncAgilityWithStreamedResponse: def __init__(self, client: AsyncAgility) -> None: - self.assistants = resources.AsyncAssistantsResourceWithStreamingResponse(client.assistants) - self.knowledge_bases = resources.AsyncKnowledgeBasesResourceWithStreamingResponse(client.knowledge_bases) - self.users = resources.AsyncUsersResourceWithStreamingResponse(client.users) - self.threads = resources.AsyncThreadsResourceWithStreamingResponse(client.threads) - self.integrations = resources.AsyncIntegrationsResourceWithStreamingResponse(client.integrations) + self.assistants = assistants.AsyncAssistantsResourceWithStreamingResponse(client.assistants) + self.knowledge_bases = knowledge_bases.AsyncKnowledgeBasesResourceWithStreamingResponse(client.knowledge_bases) + self.users = users.AsyncUsersResourceWithStreamingResponse(client.users) + self.threads = threads.AsyncThreadsResourceWithStreamingResponse(client.threads) + self.integrations = integrations.AsyncIntegrationsResourceWithStreamingResponse(client.integrations) Client = Agility diff --git a/src/agility/resources/knowledge_bases/knowledge_bases.py b/src/agility/resources/knowledge_bases/knowledge_bases.py index 807d159..02681c0 100644 --- a/src/agility/resources/knowledge_bases/knowledge_bases.py +++ b/src/agility/resources/knowledge_bases/knowledge_bases.py @@ -9,14 +9,6 @@ knowledge_base_create_params, knowledge_base_update_params, ) -from .sources import ( - SourcesResource, - AsyncSourcesResource, - SourcesResourceWithRawResponse, - AsyncSourcesResourceWithRawResponse, - SourcesResourceWithStreamingResponse, - AsyncSourcesResourceWithStreamingResponse, -) from ..._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven from ..._utils import ( maybe_transform, @@ -32,7 +24,14 @@ ) from ...pagination import SyncMyOffsetPage, AsyncMyOffsetPage from ..._base_client import AsyncPaginator, make_request_options -from .sources.sources import SourcesResource, AsyncSourcesResource +from .sources.sources import ( + SourcesResource, + AsyncSourcesResource, + SourcesResourceWithRawResponse, + AsyncSourcesResourceWithRawResponse, + SourcesResourceWithStreamingResponse, + AsyncSourcesResourceWithStreamingResponse, +) from ...types.knowledge_base_with_config import KnowledgeBaseWithConfig from ...types.knowledge_base_list_response import KnowledgeBaseListResponse From edb7f0d4945ced7ad3cf31175edbe8ed8aed1cc2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Dec 2024 07:32:37 +0000 Subject: [PATCH 36/77] chore(internal): codegen related update --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index cc9016c..b14b483 100644 --- a/README.md +++ b/README.md @@ -382,6 +382,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 agility import Agility + +with Agility() 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 62ed775e8abc1c9be2e0e7704cd8e3efc139784c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Dec 2024 07:33:31 +0000 Subject: [PATCH 37/77] chore(internal): codegen related update --- README.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/README.md b/README.md index b14b483..cc9016c 100644 --- a/README.md +++ b/README.md @@ -382,16 +382,6 @@ 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 agility import Agility - -with Agility() 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 9198bfe695b804597875e34b7779d424e11ff42a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Dec 2024 07:36:49 +0000 Subject: [PATCH 38/77] docs(readme): example snippet for client context manager --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index cc9016c..b14b483 100644 --- a/README.md +++ b/README.md @@ -382,6 +382,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 agility import Agility + +with Agility() 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 4a6fa02ab441d1e7d4f83f147b63c09d94787415 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 18 Dec 2024 07:30:39 +0000 Subject: [PATCH 39/77] chore(internal): fix some typos --- 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 afe7f9f..92cb3a5 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -343,11 +343,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( @@ -1125,11 +1125,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 a2d95d7b839417432fd85e194337af3b67e51fdd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 18 Dec 2024 23:24:49 +0000 Subject: [PATCH 40/77] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index e8a0030..4b6c22d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 42 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-a5e85dccbbe3de5dd205ee7b7952430a69328e5ac7867e79f3ed638e2e818bc5.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-3ae3cc19d925c687fd7fbdc03f6c149464f945110b175502fa096d7b59f3fbdd.yml From fc0ebaa01b76b5e91359df387644dedabebcedad Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 2 Jan 2025 00:13:34 +0000 Subject: [PATCH 41/77] feat(api): api update --- .stats.yml | 2 +- LICENSE | 2 +- .../resources/assistants/assistants.py | 10 ++++++- src/agility/resources/threads/runs.py | 8 +++++ src/agility/types/assistant_create_params.py | 23 ++++++++++++-- src/agility/types/assistant_list_response.py | 23 ++++++++++++-- src/agility/types/assistant_update_params.py | 23 ++++++++++++-- src/agility/types/assistant_with_config.py | 23 ++++++++++++-- src/agility/types/threads/run.py | 23 ++++++++++++-- .../types/threads/run_create_params.py | 30 +++++++++++++++++-- .../types/threads/run_stream_params.py | 30 +++++++++++++++++-- tests/api_resources/test_assistants.py | 28 +++++++++++++++++ tests/api_resources/threads/test_runs.py | 28 +++++++++++++++++ 13 files changed, 229 insertions(+), 24 deletions(-) diff --git a/.stats.yml b/.stats.yml index 4b6c22d..53e5b22 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 42 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-3ae3cc19d925c687fd7fbdc03f6c149464f945110b175502fa096d7b59f3fbdd.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-60c8c820f2cbc6b5b0b129ccf707ec246921b9e93cfb18f4ed47a96cc97c47f8.yml diff --git a/LICENSE b/LICENSE index 77211a2..ed57031 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 Agility + Copyright 2025 Agility 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/src/agility/resources/assistants/assistants.py b/src/agility/resources/assistants/assistants.py index 4acf958..69d40fe 100644 --- a/src/agility/resources/assistants/assistants.py +++ b/src/agility/resources/assistants/assistants.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import List, Optional +from typing import List, Iterable, Optional from typing_extensions import Literal import httpx @@ -72,6 +72,7 @@ def create( instructions: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, suggested_questions: List[str] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[assistant_create_params.Tool]] | NotGiven = NOT_GIVEN, url_slug: Optional[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. @@ -113,6 +114,7 @@ def create( "instructions": instructions, "model": model, "suggested_questions": suggested_questions, + "tools": tools, "url_slug": url_slug, }, assistant_create_params.AssistantCreateParams, @@ -168,6 +170,7 @@ def update( instructions: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, suggested_questions: List[str] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[assistant_update_params.Tool]] | NotGiven = NOT_GIVEN, url_slug: Optional[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. @@ -212,6 +215,7 @@ def update( "instructions": instructions, "model": model, "suggested_questions": suggested_questions, + "tools": tools, "url_slug": url_slug, }, assistant_update_params.AssistantUpdateParams, @@ -334,6 +338,7 @@ async def create( instructions: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, suggested_questions: List[str] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[assistant_create_params.Tool]] | NotGiven = NOT_GIVEN, url_slug: Optional[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. @@ -375,6 +380,7 @@ async def create( "instructions": instructions, "model": model, "suggested_questions": suggested_questions, + "tools": tools, "url_slug": url_slug, }, assistant_create_params.AssistantCreateParams, @@ -430,6 +436,7 @@ async def update( instructions: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, suggested_questions: List[str] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[assistant_update_params.Tool]] | NotGiven = NOT_GIVEN, url_slug: Optional[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. @@ -474,6 +481,7 @@ async def update( "instructions": instructions, "model": model, "suggested_questions": suggested_questions, + "tools": tools, "url_slug": url_slug, }, assistant_update_params.AssistantUpdateParams, diff --git a/src/agility/resources/threads/runs.py b/src/agility/resources/threads/runs.py index ae3431e..0c706ba 100644 --- a/src/agility/resources/threads/runs.py +++ b/src/agility/resources/threads/runs.py @@ -58,6 +58,7 @@ def create( instructions: Optional[str] | NotGiven = NOT_GIVEN, knowledge_base_id: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[run_create_params.Tool]] | 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, @@ -92,6 +93,7 @@ def create( "instructions": instructions, "knowledge_base_id": knowledge_base_id, "model": model, + "tools": tools, }, run_create_params.RunCreateParams, ), @@ -185,6 +187,7 @@ def stream( instructions: Optional[str] | NotGiven = NOT_GIVEN, knowledge_base_id: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[run_stream_params.Tool]] | 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, @@ -219,6 +222,7 @@ def stream( "instructions": instructions, "knowledge_base_id": knowledge_base_id, "model": model, + "tools": tools, }, run_stream_params.RunStreamParams, ), @@ -260,6 +264,7 @@ async def create( instructions: Optional[str] | NotGiven = NOT_GIVEN, knowledge_base_id: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[run_create_params.Tool]] | 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, @@ -294,6 +299,7 @@ async def create( "instructions": instructions, "knowledge_base_id": knowledge_base_id, "model": model, + "tools": tools, }, run_create_params.RunCreateParams, ), @@ -387,6 +393,7 @@ async def stream( instructions: Optional[str] | NotGiven = NOT_GIVEN, knowledge_base_id: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, + tools: Optional[Iterable[run_stream_params.Tool]] | 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, @@ -421,6 +428,7 @@ async def stream( "instructions": instructions, "knowledge_base_id": knowledge_base_id, "model": model, + "tools": tools, }, run_stream_params.RunStreamParams, ), diff --git a/src/agility/types/assistant_create_params.py b/src/agility/types/assistant_create_params.py index 301b279..53b5790 100644 --- a/src/agility/types/assistant_create_params.py +++ b/src/agility/types/assistant_create_params.py @@ -2,10 +2,10 @@ from __future__ import annotations -from typing import List, Optional -from typing_extensions import Literal, Required, TypedDict +from typing import List, Union, Iterable, Optional +from typing_extensions import Literal, Required, TypeAlias, TypedDict -__all__ = ["AssistantCreateParams"] +__all__ = ["AssistantCreateParams", "Tool", "ToolAlphaV0Tool", "ToolNoOpTool"] class AssistantCreateParams(TypedDict, total=False): @@ -27,5 +27,22 @@ class AssistantCreateParams(TypedDict, total=False): suggested_questions: List[str] """A list of suggested questions that can be asked to the assistant""" + tools: Optional[Iterable[Tool]] + url_slug: Optional[str] """Optional URL suffix - unique identifier for the assistant's endpoint""" + + +class ToolAlphaV0Tool(TypedDict, total=False): + access_key: Required[str] + + project_id: Required[int] + + name: Literal["alpha_v0"] + + +class ToolNoOpTool(TypedDict, total=False): + name: Literal["noop"] + + +Tool: TypeAlias = Union[ToolAlphaV0Tool, ToolNoOpTool] diff --git a/src/agility/types/assistant_list_response.py b/src/agility/types/assistant_list_response.py index 1119326..ddd1f80 100644 --- a/src/agility/types/assistant_list_response.py +++ b/src/agility/types/assistant_list_response.py @@ -1,12 +1,27 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List, Optional +from typing import List, Union, Optional from datetime import datetime -from typing_extensions import Literal +from typing_extensions import Literal, TypeAlias from .._models import BaseModel -__all__ = ["AssistantListResponse"] +__all__ = ["AssistantListResponse", "Tool", "ToolAlphaV0Tool", "ToolNoOpTool"] + + +class ToolAlphaV0Tool(BaseModel): + access_key: str + + project_id: int + + name: Optional[Literal["alpha_v0"]] = None + + +class ToolNoOpTool(BaseModel): + name: Optional[Literal["noop"]] = None + + +Tool: TypeAlias = Union[ToolAlphaV0Tool, ToolNoOpTool] class AssistantListResponse(BaseModel): @@ -38,5 +53,7 @@ class AssistantListResponse(BaseModel): suggested_questions: Optional[List[str]] = None """A list of suggested questions that can be asked to the assistant""" + tools: Optional[List[Tool]] = None + url_slug: Optional[str] = None """Optional URL suffix - unique identifier for the assistant's endpoint""" diff --git a/src/agility/types/assistant_update_params.py b/src/agility/types/assistant_update_params.py index 329a598..d107d1d 100644 --- a/src/agility/types/assistant_update_params.py +++ b/src/agility/types/assistant_update_params.py @@ -2,10 +2,10 @@ from __future__ import annotations -from typing import List, Optional -from typing_extensions import Literal, Required, TypedDict +from typing import List, Union, Iterable, Optional +from typing_extensions import Literal, Required, TypeAlias, TypedDict -__all__ = ["AssistantUpdateParams"] +__all__ = ["AssistantUpdateParams", "Tool", "ToolAlphaV0Tool", "ToolNoOpTool"] class AssistantUpdateParams(TypedDict, total=False): @@ -29,5 +29,22 @@ class AssistantUpdateParams(TypedDict, total=False): suggested_questions: List[str] """A list of suggested questions that can be asked to the assistant""" + tools: Optional[Iterable[Tool]] + url_slug: Optional[str] """Optional URL suffix - unique identifier for the assistant's endpoint""" + + +class ToolAlphaV0Tool(TypedDict, total=False): + access_key: Required[str] + + project_id: Required[int] + + name: Literal["alpha_v0"] + + +class ToolNoOpTool(TypedDict, total=False): + name: Literal["noop"] + + +Tool: TypeAlias = Union[ToolAlphaV0Tool, ToolNoOpTool] diff --git a/src/agility/types/assistant_with_config.py b/src/agility/types/assistant_with_config.py index e3e1561..23a651d 100644 --- a/src/agility/types/assistant_with_config.py +++ b/src/agility/types/assistant_with_config.py @@ -1,12 +1,27 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List, Optional +from typing import List, Union, Optional from datetime import datetime -from typing_extensions import Literal +from typing_extensions import Literal, TypeAlias from .._models import BaseModel -__all__ = ["AssistantWithConfig"] +__all__ = ["AssistantWithConfig", "Tool", "ToolAlphaV0Tool", "ToolNoOpTool"] + + +class ToolAlphaV0Tool(BaseModel): + access_key: str + + project_id: int + + name: Optional[Literal["alpha_v0"]] = None + + +class ToolNoOpTool(BaseModel): + name: Optional[Literal["noop"]] = None + + +Tool: TypeAlias = Union[ToolAlphaV0Tool, ToolNoOpTool] class AssistantWithConfig(BaseModel): @@ -36,5 +51,7 @@ class AssistantWithConfig(BaseModel): suggested_questions: Optional[List[str]] = None """A list of suggested questions that can be asked to the assistant""" + tools: Optional[List[Tool]] = None + url_slug: Optional[str] = None """Optional URL suffix - unique identifier for the assistant's endpoint""" diff --git a/src/agility/types/threads/run.py b/src/agility/types/threads/run.py index cbb8f45..627f62b 100644 --- a/src/agility/types/threads/run.py +++ b/src/agility/types/threads/run.py @@ -1,12 +1,27 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional +from typing import List, Union, Optional from datetime import datetime -from typing_extensions import Literal +from typing_extensions import Literal, TypeAlias from ..._models import BaseModel -__all__ = ["Run", "Usage"] +__all__ = ["Run", "Tool", "ToolAlphaV0Tool", "ToolNoOpTool", "Usage"] + + +class ToolAlphaV0Tool(BaseModel): + access_key: str + + project_id: int + + name: Optional[Literal["alpha_v0"]] = None + + +class ToolNoOpTool(BaseModel): + name: Optional[Literal["noop"]] = None + + +Tool: TypeAlias = Union[ToolAlphaV0Tool, ToolNoOpTool] class Usage(BaseModel): @@ -45,4 +60,6 @@ class Run(BaseModel): model: Optional[Literal["gpt-4o"]] = None + tools: Optional[List[Tool]] = None + usage: Optional[Usage] = None diff --git a/src/agility/types/threads/run_create_params.py b/src/agility/types/threads/run_create_params.py index a8b1082..6ab6ba2 100644 --- a/src/agility/types/threads/run_create_params.py +++ b/src/agility/types/threads/run_create_params.py @@ -2,10 +2,17 @@ from __future__ import annotations -from typing import List, Iterable, Optional -from typing_extensions import Literal, Required, TypedDict +from typing import List, Union, Iterable, Optional +from typing_extensions import Literal, Required, TypeAlias, TypedDict -__all__ = ["RunCreateParams", "AdditionalMessage", "AdditionalMessageMetadata"] +__all__ = [ + "RunCreateParams", + "AdditionalMessage", + "AdditionalMessageMetadata", + "Tool", + "ToolAlphaV0Tool", + "ToolNoOpTool", +] class RunCreateParams(TypedDict, total=False): @@ -24,6 +31,8 @@ class RunCreateParams(TypedDict, total=False): model: Optional[Literal["gpt-4o"]] + tools: Optional[Iterable[Tool]] + class AdditionalMessageMetadata(TypedDict, total=False): citations: Optional[List[str]] @@ -39,3 +48,18 @@ class AdditionalMessage(TypedDict, total=False): role: Required[Literal["user", "assistant"]] thread_id: Required[str] + + +class ToolAlphaV0Tool(TypedDict, total=False): + access_key: Required[str] + + project_id: Required[int] + + name: Literal["alpha_v0"] + + +class ToolNoOpTool(TypedDict, total=False): + name: Literal["noop"] + + +Tool: TypeAlias = Union[ToolAlphaV0Tool, ToolNoOpTool] diff --git a/src/agility/types/threads/run_stream_params.py b/src/agility/types/threads/run_stream_params.py index dd96db3..eecc6a5 100644 --- a/src/agility/types/threads/run_stream_params.py +++ b/src/agility/types/threads/run_stream_params.py @@ -2,10 +2,17 @@ from __future__ import annotations -from typing import List, Iterable, Optional -from typing_extensions import Literal, Required, TypedDict +from typing import List, Union, Iterable, Optional +from typing_extensions import Literal, Required, TypeAlias, TypedDict -__all__ = ["RunStreamParams", "AdditionalMessage", "AdditionalMessageMetadata"] +__all__ = [ + "RunStreamParams", + "AdditionalMessage", + "AdditionalMessageMetadata", + "Tool", + "ToolAlphaV0Tool", + "ToolNoOpTool", +] class RunStreamParams(TypedDict, total=False): @@ -24,6 +31,8 @@ class RunStreamParams(TypedDict, total=False): model: Optional[Literal["gpt-4o"]] + tools: Optional[Iterable[Tool]] + class AdditionalMessageMetadata(TypedDict, total=False): citations: Optional[List[str]] @@ -39,3 +48,18 @@ class AdditionalMessage(TypedDict, total=False): role: Required[Literal["user", "assistant"]] thread_id: Required[str] + + +class ToolAlphaV0Tool(TypedDict, total=False): + access_key: Required[str] + + project_id: Required[int] + + name: Literal["alpha_v0"] + + +class ToolNoOpTool(TypedDict, total=False): + name: Literal["noop"] + + +Tool: TypeAlias = Union[ToolAlphaV0Tool, ToolNoOpTool] diff --git a/tests/api_resources/test_assistants.py b/tests/api_resources/test_assistants.py index 80a5916..8dcc938 100644 --- a/tests/api_resources/test_assistants.py +++ b/tests/api_resources/test_assistants.py @@ -41,6 +41,13 @@ def test_method_create_with_all_params(self, client: Agility) -> None: instructions="instructions", model="gpt-4o", suggested_questions=["string"], + tools=[ + { + "access_key": "access_key", + "project_id": 0, + "name": "alpha_v0", + } + ], url_slug="url_slug", ) assert_matches_type(Assistant, assistant, path=["response"]) @@ -134,6 +141,13 @@ def test_method_update_with_all_params(self, client: Agility) -> None: instructions="instructions", model="gpt-4o", suggested_questions=["string"], + tools=[ + { + "access_key": "access_key", + "project_id": 0, + "name": "alpha_v0", + } + ], url_slug="url_slug", ) assert_matches_type(AssistantWithConfig, assistant, path=["response"]) @@ -275,6 +289,13 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgility) - instructions="instructions", model="gpt-4o", suggested_questions=["string"], + tools=[ + { + "access_key": "access_key", + "project_id": 0, + "name": "alpha_v0", + } + ], url_slug="url_slug", ) assert_matches_type(Assistant, assistant, path=["response"]) @@ -368,6 +389,13 @@ async def test_method_update_with_all_params(self, async_client: AsyncAgility) - instructions="instructions", model="gpt-4o", suggested_questions=["string"], + tools=[ + { + "access_key": "access_key", + "project_id": 0, + "name": "alpha_v0", + } + ], url_slug="url_slug", ) assert_matches_type(AssistantWithConfig, assistant, path=["response"]) diff --git a/tests/api_resources/threads/test_runs.py b/tests/api_resources/threads/test_runs.py index 5b509e3..4de25cf 100644 --- a/tests/api_resources/threads/test_runs.py +++ b/tests/api_resources/threads/test_runs.py @@ -46,6 +46,13 @@ def test_method_create_with_all_params(self, client: Agility) -> None: instructions="instructions", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", model="gpt-4o", + tools=[ + { + "access_key": "access_key", + "project_id": 0, + "name": "alpha_v0", + } + ], ) assert_matches_type(Run, run, path=["response"]) @@ -208,6 +215,13 @@ def test_method_stream_with_all_params(self, client: Agility) -> None: instructions="instructions", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", model="gpt-4o", + tools=[ + { + "access_key": "access_key", + "project_id": 0, + "name": "alpha_v0", + } + ], ) assert_matches_type(object, run, path=["response"]) @@ -278,6 +292,13 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgility) - instructions="instructions", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", model="gpt-4o", + tools=[ + { + "access_key": "access_key", + "project_id": 0, + "name": "alpha_v0", + } + ], ) assert_matches_type(Run, run, path=["response"]) @@ -440,6 +461,13 @@ async def test_method_stream_with_all_params(self, async_client: AsyncAgility) - instructions="instructions", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", model="gpt-4o", + tools=[ + { + "access_key": "access_key", + "project_id": 0, + "name": "alpha_v0", + } + ], ) assert_matches_type(object, run, path=["response"]) From 512933040b3086d81feb1d2bf3f1464f3245700c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Jan 2025 03:23:31 +0000 Subject: [PATCH 42/77] chore(internal): codegen related update --- src/agility/_models.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/agility/_models.py b/src/agility/_models.py index 7a547ce..d56ea1d 100644 --- a/src/agility/_models.py +++ b/src/agility/_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 1f18f9ab6e21acae06b0fd887424c8b35262e7ef Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 8 Jan 2025 03:37:25 +0000 Subject: [PATCH 43/77] chore(internal): bump httpx dependency --- 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 906db9e..ebb43dd 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 77bf3cc..7cb01f1 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 agility # via respx 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 @@ -86,7 +86,6 @@ six==1.16.0 sniffio==1.3.0 # via agility # via anyio - # via httpx time-machine==2.9.0 tomli==2.0.2 # via mypy diff --git a/requirements.lock b/requirements.lock index 034dcf4..b2ab8b1 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 agility idna==3.4 # via anyio @@ -37,7 +37,6 @@ pydantic-core==2.27.1 sniffio==1.3.0 # via agility # via anyio - # via httpx typing-extensions==4.12.2 # via agility # via anyio From 12ad847ceeb345ab435e4c1dcf90d099a2eba3bb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 8 Jan 2025 03:40:56 +0000 Subject: [PATCH 44/77] fix(client): only call .close() when needed --- src/agility/_base_client.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/agility/_base_client.py b/src/agility/_base_client.py index d4410a2..b8bea80 100644 --- a/src/agility/_base_client.py +++ b/src/agility/_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 345bb90b205629d586953a9c42ee22e23cf21827 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 9 Jan 2025 03:34:19 +0000 Subject: [PATCH 45/77] docs: fix typos --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b14b483..eb9e6bb 100644 --- a/README.md +++ b/README.md @@ -177,7 +177,7 @@ except agility.APIStatusError as e: print(e.response) ``` -Error codes are as followed: +Error codes are as follows: | Status Code | Error Type | | ----------- | -------------------------- | @@ -324,8 +324,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 b01c686d8766dc618185d326da7a629f52847fec Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 9 Jan 2025 03:35:25 +0000 Subject: [PATCH 46/77] chore(internal): codegen related update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eb9e6bb..856c344 100644 --- a/README.md +++ b/README.md @@ -396,7 +396,7 @@ with Agility() 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 7832bd89f216ca764948450e8704a656bd215889 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 10 Jan 2025 03:20:00 +0000 Subject: [PATCH 47/77] chore(internal): codegen related update --- src/agility/_models.py | 8 ++++---- tests/test_models.py | 10 ++++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/agility/_models.py b/src/agility/_models.py index d56ea1d..9a918aa 100644 --- a/src/agility/_models.py +++ b/src/agility/_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 a224db3..557d4a3 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 e830a9f076e8ec3d1f7c61222be4ea005c14f490 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 15 Jan 2025 23:14:02 +0000 Subject: [PATCH 48/77] feat(api): api update --- .stats.yml | 2 +- src/agility/types/threads/message.py | 2 ++ src/agility/types/threads/message_create_params.py | 2 ++ src/agility/types/threads/run_create_params.py | 2 ++ src/agility/types/threads/run_stream_params.py | 2 ++ tests/api_resources/threads/test_messages.py | 2 ++ tests/api_resources/threads/test_runs.py | 4 ++++ 7 files changed, 15 insertions(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 53e5b22..752a1a0 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 42 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-60c8c820f2cbc6b5b0b129ccf707ec246921b9e93cfb18f4ed47a96cc97c47f8.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-92f517edd77d984ab27e6041507df81a910368e14677537005e8db34359d503b.yml diff --git a/src/agility/types/threads/message.py b/src/agility/types/threads/message.py index c5d56cf..f36e609 100644 --- a/src/agility/types/threads/message.py +++ b/src/agility/types/threads/message.py @@ -12,6 +12,8 @@ class Metadata(BaseModel): citations: Optional[List[str]] = None + trustworthiness_explanation: Optional[str] = None + trustworthiness_score: Optional[float] = None diff --git a/src/agility/types/threads/message_create_params.py b/src/agility/types/threads/message_create_params.py index cccbb06..9048755 100644 --- a/src/agility/types/threads/message_create_params.py +++ b/src/agility/types/threads/message_create_params.py @@ -19,4 +19,6 @@ class MessageCreateParams(TypedDict, total=False): class Metadata(TypedDict, total=False): citations: Optional[List[str]] + trustworthiness_explanation: Optional[str] + trustworthiness_score: Optional[float] diff --git a/src/agility/types/threads/run_create_params.py b/src/agility/types/threads/run_create_params.py index 6ab6ba2..539f11e 100644 --- a/src/agility/types/threads/run_create_params.py +++ b/src/agility/types/threads/run_create_params.py @@ -37,6 +37,8 @@ class RunCreateParams(TypedDict, total=False): class AdditionalMessageMetadata(TypedDict, total=False): citations: Optional[List[str]] + trustworthiness_explanation: Optional[str] + trustworthiness_score: Optional[float] diff --git a/src/agility/types/threads/run_stream_params.py b/src/agility/types/threads/run_stream_params.py index eecc6a5..2336731 100644 --- a/src/agility/types/threads/run_stream_params.py +++ b/src/agility/types/threads/run_stream_params.py @@ -37,6 +37,8 @@ class RunStreamParams(TypedDict, total=False): class AdditionalMessageMetadata(TypedDict, total=False): citations: Optional[List[str]] + trustworthiness_explanation: Optional[str] + trustworthiness_score: Optional[float] diff --git a/tests/api_resources/threads/test_messages.py b/tests/api_resources/threads/test_messages.py index 6c7578b..2751f99 100644 --- a/tests/api_resources/threads/test_messages.py +++ b/tests/api_resources/threads/test_messages.py @@ -35,6 +35,7 @@ def test_method_create_with_all_params(self, client: Agility) -> None: content="content", metadata={ "citations": ["string"], + "trustworthiness_explanation": "trustworthiness_explanation", "trustworthiness_score": 0, }, role="user", @@ -245,6 +246,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgility) - content="content", metadata={ "citations": ["string"], + "trustworthiness_explanation": "trustworthiness_explanation", "trustworthiness_score": 0, }, role="user", diff --git a/tests/api_resources/threads/test_runs.py b/tests/api_resources/threads/test_runs.py index 4de25cf..9cf4d5d 100644 --- a/tests/api_resources/threads/test_runs.py +++ b/tests/api_resources/threads/test_runs.py @@ -36,6 +36,7 @@ def test_method_create_with_all_params(self, client: Agility) -> None: "content": "content", "metadata": { "citations": ["string"], + "trustworthiness_explanation": "trustworthiness_explanation", "trustworthiness_score": 0, }, "role": "user", @@ -205,6 +206,7 @@ def test_method_stream_with_all_params(self, client: Agility) -> None: "content": "content", "metadata": { "citations": ["string"], + "trustworthiness_explanation": "trustworthiness_explanation", "trustworthiness_score": 0, }, "role": "user", @@ -282,6 +284,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgility) - "content": "content", "metadata": { "citations": ["string"], + "trustworthiness_explanation": "trustworthiness_explanation", "trustworthiness_score": 0, }, "role": "user", @@ -451,6 +454,7 @@ async def test_method_stream_with_all_params(self, async_client: AsyncAgility) - "content": "content", "metadata": { "citations": ["string"], + "trustworthiness_explanation": "trustworthiness_explanation", "trustworthiness_score": 0, }, "role": "user", From 882e3d2277401e46b005a38f6b44289e877c86bc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 17 Jan 2025 03:43:53 +0000 Subject: [PATCH 49/77] chore(internal): codegen related update --- mypy.ini | 2 +- requirements-dev.lock | 4 ++-- src/agility/_response.py | 8 +++++++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/mypy.ini b/mypy.ini index f353443..7a8ca92 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 7cb01f1..bb94bb9 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.392.post0 pytest==8.3.3 # via pytest-asyncio pytest-asyncio==0.24.0 diff --git a/src/agility/_response.py b/src/agility/_response.py index 00f5b74..292d433 100644 --- a/src/agility/_response.py +++ b/src/agility/_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 agility import BaseModel`") if ( From 0c7d7e3a97971ada17f4dac9f4896217e0ef7bc7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 22 Jan 2025 03:42:41 +0000 Subject: [PATCH 50/77] chore(internal): codegen related update --- pyproject.toml | 1 + .../resources/assistants/access_keys.py | 4 +-- .../resources/assistants/assistants.py | 4 +-- .../resources/integrations/available.py | 4 +-- .../resources/integrations/integrations.py | 4 +-- src/agility/resources/integrations/rbac.py | 4 +-- .../knowledge_bases/knowledge_bases.py | 4 +-- .../knowledge_bases/sources/documents.py | 4 +-- .../knowledge_bases/sources/sources.py | 4 +-- src/agility/resources/threads/messages.py | 4 +-- src/agility/resources/threads/runs.py | 4 +-- src/agility/resources/threads/threads.py | 4 +-- src/agility/resources/users/api_key.py | 4 +-- src/agility/resources/users/users.py | 4 +-- tests/test_client.py | 25 +++++++++++++------ 15 files changed, 45 insertions(+), 33 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ebb43dd..b086121 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" ] diff --git a/src/agility/resources/assistants/access_keys.py b/src/agility/resources/assistants/access_keys.py index 80d11d9..2b154fe 100644 --- a/src/agility/resources/assistants/access_keys.py +++ b/src/agility/resources/assistants/access_keys.py @@ -32,7 +32,7 @@ class AccessKeysResource(SyncAPIResource): @cached_property def with_raw_response(self) -> AccessKeysResourceWithRawResponse: """ - 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers @@ -143,7 +143,7 @@ class AsyncAccessKeysResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncAccessKeysResourceWithRawResponse: """ - 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers diff --git a/src/agility/resources/assistants/assistants.py b/src/agility/resources/assistants/assistants.py index 69d40fe..dc25263 100644 --- a/src/agility/resources/assistants/assistants.py +++ b/src/agility/resources/assistants/assistants.py @@ -46,7 +46,7 @@ def access_keys(self) -> AccessKeysResource: @cached_property def with_raw_response(self) -> AssistantsResourceWithRawResponse: """ - 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers @@ -312,7 +312,7 @@ def access_keys(self) -> AsyncAccessKeysResource: @cached_property def with_raw_response(self) -> AsyncAssistantsResourceWithRawResponse: """ - 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers diff --git a/src/agility/resources/integrations/available.py b/src/agility/resources/integrations/available.py index 0f77dea..380c3d3 100644 --- a/src/agility/resources/integrations/available.py +++ b/src/agility/resources/integrations/available.py @@ -23,7 +23,7 @@ class AvailableResource(SyncAPIResource): @cached_property def with_raw_response(self) -> AvailableResourceWithRawResponse: """ - 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers @@ -63,7 +63,7 @@ class AsyncAvailableResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncAvailableResourceWithRawResponse: """ - 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers diff --git a/src/agility/resources/integrations/integrations.py b/src/agility/resources/integrations/integrations.py index 29d0539..536398a 100644 --- a/src/agility/resources/integrations/integrations.py +++ b/src/agility/resources/integrations/integrations.py @@ -57,7 +57,7 @@ def rbac(self) -> RbacResource: @cached_property def with_raw_response(self) -> IntegrationsResourceWithRawResponse: """ - 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers @@ -242,7 +242,7 @@ def rbac(self) -> AsyncRbacResource: @cached_property def with_raw_response(self) -> AsyncIntegrationsResourceWithRawResponse: """ - 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers diff --git a/src/agility/resources/integrations/rbac.py b/src/agility/resources/integrations/rbac.py index a758cde..40e690a 100644 --- a/src/agility/resources/integrations/rbac.py +++ b/src/agility/resources/integrations/rbac.py @@ -23,7 +23,7 @@ class RbacResource(SyncAPIResource): @cached_property def with_raw_response(self) -> RbacResourceWithRawResponse: """ - 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers @@ -77,7 +77,7 @@ class AsyncRbacResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncRbacResourceWithRawResponse: """ - 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers diff --git a/src/agility/resources/knowledge_bases/knowledge_bases.py b/src/agility/resources/knowledge_bases/knowledge_bases.py index 02681c0..737dd47 100644 --- a/src/agility/resources/knowledge_bases/knowledge_bases.py +++ b/src/agility/resources/knowledge_bases/knowledge_bases.py @@ -46,7 +46,7 @@ def sources(self) -> SourcesResource: @cached_property def with_raw_response(self) -> KnowledgeBasesResourceWithRawResponse: """ - 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers @@ -274,7 +274,7 @@ def sources(self) -> AsyncSourcesResource: @cached_property def with_raw_response(self) -> AsyncKnowledgeBasesResourceWithRawResponse: """ - 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers diff --git a/src/agility/resources/knowledge_bases/sources/documents.py b/src/agility/resources/knowledge_bases/sources/documents.py index e788eeb..05a3833 100644 --- a/src/agility/resources/knowledge_bases/sources/documents.py +++ b/src/agility/resources/knowledge_bases/sources/documents.py @@ -26,7 +26,7 @@ class DocumentsResource(SyncAPIResource): @cached_property def with_raw_response(self) -> DocumentsResourceWithRawResponse: """ - 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers @@ -135,7 +135,7 @@ class AsyncDocumentsResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncDocumentsResourceWithRawResponse: """ - 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers diff --git a/src/agility/resources/knowledge_bases/sources/sources.py b/src/agility/resources/knowledge_bases/sources/sources.py index 93f8987..06bc2a6 100644 --- a/src/agility/resources/knowledge_bases/sources/sources.py +++ b/src/agility/resources/knowledge_bases/sources/sources.py @@ -42,7 +42,7 @@ def documents(self) -> DocumentsResource: @cached_property def with_raw_response(self) -> SourcesResourceWithRawResponse: """ - 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers @@ -371,7 +371,7 @@ def documents(self) -> AsyncDocumentsResource: @cached_property def with_raw_response(self) -> AsyncSourcesResourceWithRawResponse: """ - 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers diff --git a/src/agility/resources/threads/messages.py b/src/agility/resources/threads/messages.py index 2b7d7f3..c1e7fd9 100644 --- a/src/agility/resources/threads/messages.py +++ b/src/agility/resources/threads/messages.py @@ -32,7 +32,7 @@ class MessagesResource(SyncAPIResource): @cached_property def with_raw_response(self) -> MessagesResourceWithRawResponse: """ - 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers @@ -216,7 +216,7 @@ class AsyncMessagesResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncMessagesResourceWithRawResponse: """ - 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers diff --git a/src/agility/resources/threads/runs.py b/src/agility/resources/threads/runs.py index 0c706ba..f515e92 100644 --- a/src/agility/resources/threads/runs.py +++ b/src/agility/resources/threads/runs.py @@ -31,7 +31,7 @@ class RunsResource(SyncAPIResource): @cached_property def with_raw_response(self) -> RunsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers @@ -237,7 +237,7 @@ class AsyncRunsResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncRunsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers diff --git a/src/agility/resources/threads/threads.py b/src/agility/resources/threads/threads.py index 6d57caa..de35eef 100644 --- a/src/agility/resources/threads/threads.py +++ b/src/agility/resources/threads/threads.py @@ -50,7 +50,7 @@ def runs(self) -> RunsResource: @cached_property def with_raw_response(self) -> ThreadsResourceWithRawResponse: """ - 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers @@ -208,7 +208,7 @@ def runs(self) -> AsyncRunsResource: @cached_property def with_raw_response(self) -> AsyncThreadsResourceWithRawResponse: """ - 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers diff --git a/src/agility/resources/users/api_key.py b/src/agility/resources/users/api_key.py index c21e5ff..fa6cac6 100644 --- a/src/agility/resources/users/api_key.py +++ b/src/agility/resources/users/api_key.py @@ -23,7 +23,7 @@ class APIKeyResource(SyncAPIResource): @cached_property def with_raw_response(self) -> APIKeyResourceWithRawResponse: """ - 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers @@ -110,7 +110,7 @@ class AsyncAPIKeyResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncAPIKeyResourceWithRawResponse: """ - 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers diff --git a/src/agility/resources/users/users.py b/src/agility/resources/users/users.py index 16a7b3d..ceea91c 100644 --- a/src/agility/resources/users/users.py +++ b/src/agility/resources/users/users.py @@ -35,7 +35,7 @@ def api_key(self) -> APIKeyResource: @cached_property def with_raw_response(self) -> UsersResourceWithRawResponse: """ - 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers @@ -93,7 +93,7 @@ def api_key(self) -> AsyncAPIKeyResource: @cached_property def with_raw_response(self) -> AsyncUsersResourceWithRawResponse: """ - 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/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers diff --git a/tests/test_client.py b/tests/test_client.py index 92cb3a5..5927f2d 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 @@ -1661,10 +1662,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 e1041db2474857eb4c4e0316e5e304b660e86620 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 22 Jan 2025 19:13:58 +0000 Subject: [PATCH 51/77] feat(api): api update --- .stats.yml | 2 +- src/agility/types/assistant_create_params.py | 12 +++++------- src/agility/types/assistant_list_response.py | 12 +++++------- src/agility/types/assistant_update_params.py | 12 +++++------- src/agility/types/assistant_with_config.py | 12 +++++------- src/agility/types/threads/run.py | 12 +++++------- src/agility/types/threads/run_create_params.py | 12 +++++------- src/agility/types/threads/run_stream_params.py | 12 +++++------- tests/api_resources/test_assistants.py | 12 ++++-------- tests/api_resources/threads/test_runs.py | 12 ++++-------- 10 files changed, 44 insertions(+), 66 deletions(-) diff --git a/.stats.yml b/.stats.yml index 752a1a0..74b5480 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 42 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-92f517edd77d984ab27e6041507df81a910368e14677537005e8db34359d503b.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-2aa38a4bc52c027aa61e57c5d3767438466b7897f4463659070b6a00a9b85fcd.yml diff --git a/src/agility/types/assistant_create_params.py b/src/agility/types/assistant_create_params.py index 53b5790..b26411b 100644 --- a/src/agility/types/assistant_create_params.py +++ b/src/agility/types/assistant_create_params.py @@ -5,7 +5,7 @@ from typing import List, Union, Iterable, Optional from typing_extensions import Literal, Required, TypeAlias, TypedDict -__all__ = ["AssistantCreateParams", "Tool", "ToolAlphaV0Tool", "ToolNoOpTool"] +__all__ = ["AssistantCreateParams", "Tool", "ToolCodexV0Tool", "ToolNoOpTool"] class AssistantCreateParams(TypedDict, total=False): @@ -33,16 +33,14 @@ class AssistantCreateParams(TypedDict, total=False): """Optional URL suffix - unique identifier for the assistant's endpoint""" -class ToolAlphaV0Tool(TypedDict, total=False): +class ToolCodexV0Tool(TypedDict, total=False): access_key: Required[str] - project_id: Required[int] - - name: Literal["alpha_v0"] + type: Literal["codex_v0"] class ToolNoOpTool(TypedDict, total=False): - name: Literal["noop"] + type: Literal["noop"] -Tool: TypeAlias = Union[ToolAlphaV0Tool, ToolNoOpTool] +Tool: TypeAlias = Union[ToolCodexV0Tool, ToolNoOpTool] diff --git a/src/agility/types/assistant_list_response.py b/src/agility/types/assistant_list_response.py index ddd1f80..c2d99b8 100644 --- a/src/agility/types/assistant_list_response.py +++ b/src/agility/types/assistant_list_response.py @@ -6,22 +6,20 @@ from .._models import BaseModel -__all__ = ["AssistantListResponse", "Tool", "ToolAlphaV0Tool", "ToolNoOpTool"] +__all__ = ["AssistantListResponse", "Tool", "ToolCodexV0Tool", "ToolNoOpTool"] -class ToolAlphaV0Tool(BaseModel): +class ToolCodexV0Tool(BaseModel): access_key: str - project_id: int - - name: Optional[Literal["alpha_v0"]] = None + type: Optional[Literal["codex_v0"]] = None class ToolNoOpTool(BaseModel): - name: Optional[Literal["noop"]] = None + type: Optional[Literal["noop"]] = None -Tool: TypeAlias = Union[ToolAlphaV0Tool, ToolNoOpTool] +Tool: TypeAlias = Union[ToolCodexV0Tool, ToolNoOpTool] class AssistantListResponse(BaseModel): diff --git a/src/agility/types/assistant_update_params.py b/src/agility/types/assistant_update_params.py index d107d1d..8725c27 100644 --- a/src/agility/types/assistant_update_params.py +++ b/src/agility/types/assistant_update_params.py @@ -5,7 +5,7 @@ from typing import List, Union, Iterable, Optional from typing_extensions import Literal, Required, TypeAlias, TypedDict -__all__ = ["AssistantUpdateParams", "Tool", "ToolAlphaV0Tool", "ToolNoOpTool"] +__all__ = ["AssistantUpdateParams", "Tool", "ToolCodexV0Tool", "ToolNoOpTool"] class AssistantUpdateParams(TypedDict, total=False): @@ -35,16 +35,14 @@ class AssistantUpdateParams(TypedDict, total=False): """Optional URL suffix - unique identifier for the assistant's endpoint""" -class ToolAlphaV0Tool(TypedDict, total=False): +class ToolCodexV0Tool(TypedDict, total=False): access_key: Required[str] - project_id: Required[int] - - name: Literal["alpha_v0"] + type: Literal["codex_v0"] class ToolNoOpTool(TypedDict, total=False): - name: Literal["noop"] + type: Literal["noop"] -Tool: TypeAlias = Union[ToolAlphaV0Tool, ToolNoOpTool] +Tool: TypeAlias = Union[ToolCodexV0Tool, ToolNoOpTool] diff --git a/src/agility/types/assistant_with_config.py b/src/agility/types/assistant_with_config.py index 23a651d..a87998a 100644 --- a/src/agility/types/assistant_with_config.py +++ b/src/agility/types/assistant_with_config.py @@ -6,22 +6,20 @@ from .._models import BaseModel -__all__ = ["AssistantWithConfig", "Tool", "ToolAlphaV0Tool", "ToolNoOpTool"] +__all__ = ["AssistantWithConfig", "Tool", "ToolCodexV0Tool", "ToolNoOpTool"] -class ToolAlphaV0Tool(BaseModel): +class ToolCodexV0Tool(BaseModel): access_key: str - project_id: int - - name: Optional[Literal["alpha_v0"]] = None + type: Optional[Literal["codex_v0"]] = None class ToolNoOpTool(BaseModel): - name: Optional[Literal["noop"]] = None + type: Optional[Literal["noop"]] = None -Tool: TypeAlias = Union[ToolAlphaV0Tool, ToolNoOpTool] +Tool: TypeAlias = Union[ToolCodexV0Tool, ToolNoOpTool] class AssistantWithConfig(BaseModel): diff --git a/src/agility/types/threads/run.py b/src/agility/types/threads/run.py index 627f62b..097e798 100644 --- a/src/agility/types/threads/run.py +++ b/src/agility/types/threads/run.py @@ -6,22 +6,20 @@ from ..._models import BaseModel -__all__ = ["Run", "Tool", "ToolAlphaV0Tool", "ToolNoOpTool", "Usage"] +__all__ = ["Run", "Tool", "ToolCodexV0Tool", "ToolNoOpTool", "Usage"] -class ToolAlphaV0Tool(BaseModel): +class ToolCodexV0Tool(BaseModel): access_key: str - project_id: int - - name: Optional[Literal["alpha_v0"]] = None + type: Optional[Literal["codex_v0"]] = None class ToolNoOpTool(BaseModel): - name: Optional[Literal["noop"]] = None + type: Optional[Literal["noop"]] = None -Tool: TypeAlias = Union[ToolAlphaV0Tool, ToolNoOpTool] +Tool: TypeAlias = Union[ToolCodexV0Tool, ToolNoOpTool] class Usage(BaseModel): diff --git a/src/agility/types/threads/run_create_params.py b/src/agility/types/threads/run_create_params.py index 539f11e..7bd31b2 100644 --- a/src/agility/types/threads/run_create_params.py +++ b/src/agility/types/threads/run_create_params.py @@ -10,7 +10,7 @@ "AdditionalMessage", "AdditionalMessageMetadata", "Tool", - "ToolAlphaV0Tool", + "ToolCodexV0Tool", "ToolNoOpTool", ] @@ -52,16 +52,14 @@ class AdditionalMessage(TypedDict, total=False): thread_id: Required[str] -class ToolAlphaV0Tool(TypedDict, total=False): +class ToolCodexV0Tool(TypedDict, total=False): access_key: Required[str] - project_id: Required[int] - - name: Literal["alpha_v0"] + type: Literal["codex_v0"] class ToolNoOpTool(TypedDict, total=False): - name: Literal["noop"] + type: Literal["noop"] -Tool: TypeAlias = Union[ToolAlphaV0Tool, ToolNoOpTool] +Tool: TypeAlias = Union[ToolCodexV0Tool, ToolNoOpTool] diff --git a/src/agility/types/threads/run_stream_params.py b/src/agility/types/threads/run_stream_params.py index 2336731..e13d49b 100644 --- a/src/agility/types/threads/run_stream_params.py +++ b/src/agility/types/threads/run_stream_params.py @@ -10,7 +10,7 @@ "AdditionalMessage", "AdditionalMessageMetadata", "Tool", - "ToolAlphaV0Tool", + "ToolCodexV0Tool", "ToolNoOpTool", ] @@ -52,16 +52,14 @@ class AdditionalMessage(TypedDict, total=False): thread_id: Required[str] -class ToolAlphaV0Tool(TypedDict, total=False): +class ToolCodexV0Tool(TypedDict, total=False): access_key: Required[str] - project_id: Required[int] - - name: Literal["alpha_v0"] + type: Literal["codex_v0"] class ToolNoOpTool(TypedDict, total=False): - name: Literal["noop"] + type: Literal["noop"] -Tool: TypeAlias = Union[ToolAlphaV0Tool, ToolNoOpTool] +Tool: TypeAlias = Union[ToolCodexV0Tool, ToolNoOpTool] diff --git a/tests/api_resources/test_assistants.py b/tests/api_resources/test_assistants.py index 8dcc938..72dce68 100644 --- a/tests/api_resources/test_assistants.py +++ b/tests/api_resources/test_assistants.py @@ -44,8 +44,7 @@ def test_method_create_with_all_params(self, client: Agility) -> None: tools=[ { "access_key": "access_key", - "project_id": 0, - "name": "alpha_v0", + "type": "codex_v0", } ], url_slug="url_slug", @@ -144,8 +143,7 @@ def test_method_update_with_all_params(self, client: Agility) -> None: tools=[ { "access_key": "access_key", - "project_id": 0, - "name": "alpha_v0", + "type": "codex_v0", } ], url_slug="url_slug", @@ -292,8 +290,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgility) - tools=[ { "access_key": "access_key", - "project_id": 0, - "name": "alpha_v0", + "type": "codex_v0", } ], url_slug="url_slug", @@ -392,8 +389,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncAgility) - tools=[ { "access_key": "access_key", - "project_id": 0, - "name": "alpha_v0", + "type": "codex_v0", } ], url_slug="url_slug", diff --git a/tests/api_resources/threads/test_runs.py b/tests/api_resources/threads/test_runs.py index 9cf4d5d..3b17a5f 100644 --- a/tests/api_resources/threads/test_runs.py +++ b/tests/api_resources/threads/test_runs.py @@ -50,8 +50,7 @@ def test_method_create_with_all_params(self, client: Agility) -> None: tools=[ { "access_key": "access_key", - "project_id": 0, - "name": "alpha_v0", + "type": "codex_v0", } ], ) @@ -220,8 +219,7 @@ def test_method_stream_with_all_params(self, client: Agility) -> None: tools=[ { "access_key": "access_key", - "project_id": 0, - "name": "alpha_v0", + "type": "codex_v0", } ], ) @@ -298,8 +296,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgility) - tools=[ { "access_key": "access_key", - "project_id": 0, - "name": "alpha_v0", + "type": "codex_v0", } ], ) @@ -468,8 +465,7 @@ async def test_method_stream_with_all_params(self, async_client: AsyncAgility) - tools=[ { "access_key": "access_key", - "project_id": 0, - "name": "alpha_v0", + "type": "codex_v0", } ], ) From bdeb78894688099ff13518647f4af8dc24a5bef3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 03:11:22 +0000 Subject: [PATCH 52/77] chore(internal): codegen related update --- src/agility/_response.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/agility/_response.py b/src/agility/_response.py index 292d433..9c02d4e 100644 --- a/src/agility/_response.py +++ b/src/agility/_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 656845d801788c16ced8f3437f748268cf632b43 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 24 Jan 2025 03:17:34 +0000 Subject: [PATCH 53/77] chore(internal): minor formatting changes --- .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 4029396..c8a8a4f 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 8c5c60e..e84fe62 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 97fd526..1fe1413 100755 --- a/scripts/lint +++ b/scripts/lint @@ -9,4 +9,3 @@ rye run lint echo "==> Making sure it imports" rye run python -c 'import agility' - From 44942a9d94fa048c25d79094a8c96e357cfc3bf6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 25 Jan 2025 03:23:50 +0000 Subject: [PATCH 54/77] chore: update test examples --- .../knowledge_bases/test_sources.py | 90 +++++++++++++++---- tests/api_resources/test_integrations.py | 18 ++-- 2 files changed, 84 insertions(+), 24 deletions(-) diff --git a/tests/api_resources/knowledge_bases/test_sources.py b/tests/api_resources/knowledge_bases/test_sources.py index f4bcffa..eb1ba6c 100644 --- a/tests/api_resources/knowledge_bases/test_sources.py +++ b/tests/api_resources/knowledge_bases/test_sources.py @@ -27,7 +27,10 @@ def test_method_create(self, client: Agility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -71,7 +74,10 @@ def test_raw_response_create(self, client: Agility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -89,7 +95,10 @@ def test_streaming_response_create(self, client: Agility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -110,7 +119,10 @@ def test_path_params_create(self, client: Agility) -> None: knowledge_base_id="", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -172,7 +184,10 @@ def test_method_update(self, client: Agility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -218,7 +233,10 @@ def test_raw_response_update(self, client: Agility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -237,7 +255,10 @@ def test_streaming_response_update(self, client: Agility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -259,7 +280,10 @@ def test_path_params_update(self, client: Agility) -> None: knowledge_base_id="", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -272,7 +296,10 @@ def test_path_params_update(self, client: Agility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -480,7 +507,10 @@ async def test_method_create(self, async_client: AsyncAgility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -524,7 +554,10 @@ async def test_raw_response_create(self, async_client: AsyncAgility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -542,7 +575,10 @@ async def test_streaming_response_create(self, async_client: AsyncAgility) -> No knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -563,7 +599,10 @@ async def test_path_params_create(self, async_client: AsyncAgility) -> None: knowledge_base_id="", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -625,7 +664,10 @@ async def test_method_update(self, async_client: AsyncAgility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -671,7 +713,10 @@ async def test_raw_response_update(self, async_client: AsyncAgility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -690,7 +735,10 @@ async def test_streaming_response_update(self, async_client: AsyncAgility) -> No knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -712,7 +760,10 @@ async def test_path_params_update(self, async_client: AsyncAgility) -> None: knowledge_base_id="", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -725,7 +776,10 @@ async def test_path_params_update(self, async_client: AsyncAgility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, diff --git a/tests/api_resources/test_integrations.py b/tests/api_resources/test_integrations.py index 5b92346..82d4400 100644 --- a/tests/api_resources/test_integrations.py +++ b/tests/api_resources/test_integrations.py @@ -29,7 +29,8 @@ def test_method_create(self, client: Agility) -> None: "resource": { "bucket_name": "bucket_name", "prefix": "prefix", - } + }, + "integration_type": "s3/v0", }, ) assert_matches_type(IntegrationCreateResponse, integration, path=["response"]) @@ -56,7 +57,8 @@ def test_raw_response_create(self, client: Agility) -> None: "resource": { "bucket_name": "bucket_name", "prefix": "prefix", - } + }, + "integration_type": "s3/v0", }, ) @@ -72,7 +74,8 @@ def test_streaming_response_create(self, client: Agility) -> None: "resource": { "bucket_name": "bucket_name", "prefix": "prefix", - } + }, + "integration_type": "s3/v0", }, ) as response: assert not response.is_closed @@ -203,7 +206,8 @@ async def test_method_create(self, async_client: AsyncAgility) -> None: "resource": { "bucket_name": "bucket_name", "prefix": "prefix", - } + }, + "integration_type": "s3/v0", }, ) assert_matches_type(IntegrationCreateResponse, integration, path=["response"]) @@ -230,7 +234,8 @@ async def test_raw_response_create(self, async_client: AsyncAgility) -> None: "resource": { "bucket_name": "bucket_name", "prefix": "prefix", - } + }, + "integration_type": "s3/v0", }, ) @@ -246,7 +251,8 @@ async def test_streaming_response_create(self, async_client: AsyncAgility) -> No "resource": { "bucket_name": "bucket_name", "prefix": "prefix", - } + }, + "integration_type": "s3/v0", }, ) as response: assert not response.is_closed From 502628b53ddadcce180a94a685875e7384fd6372 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 4 Feb 2025 03:01:54 +0000 Subject: [PATCH 55/77] chore(internal): change default timeout to an int --- src/agility/_constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agility/_constants.py b/src/agility/_constants.py index a2ac3b6..6ddf2c7 100644 --- a/src/agility/_constants.py +++ b/src/agility/_constants.py @@ -6,7 +6,7 @@ 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 = httpx.Timeout(timeout=60, connect=5.0) DEFAULT_MAX_RETRIES = 2 DEFAULT_CONNECTION_LIMITS = httpx.Limits(max_connections=100, max_keepalive_connections=20) From 2c19e401db750030d7261b967e941d90380dcd35 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 4 Feb 2025 03:03:35 +0000 Subject: [PATCH 56/77] chore(internal): bummp ruff dependency --- pyproject.toml | 2 +- requirements-dev.lock | 2 +- scripts/utils/ruffen-docs.py | 4 ++-- src/agility/_models.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b086121..6d45dbd 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 bb94bb9..0d6651f 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 37b3d94..0cf2bd2 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/agility/_models.py b/src/agility/_models.py index 9a918aa..12c34b7 100644 --- a/src/agility/_models.py +++ b/src/agility/_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 c17de44ca0703dc13cda473c02539e5dea5079a0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 4 Feb 2025 19:14:10 +0000 Subject: [PATCH 57/77] feat(api): api update --- .stats.yml | 2 +- .../resources/knowledge_bases/knowledge_bases.py | 8 ++------ src/agility/types/knowledge_base_create_params.py | 13 ++----------- tests/api_resources/test_knowledge_bases.py | 12 ++++++------ 4 files changed, 11 insertions(+), 24 deletions(-) diff --git a/.stats.yml b/.stats.yml index 74b5480..573f249 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 42 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-2aa38a4bc52c027aa61e57c5d3767438466b7897f4463659070b6a00a9b85fcd.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-b8c06b2cd81f690ccbf027f4dcaf2af7597a200a3b8b58d0f53240f3b18c6bce.yml diff --git a/src/agility/resources/knowledge_bases/knowledge_bases.py b/src/agility/resources/knowledge_bases/knowledge_bases.py index 737dd47..3d6cf25 100644 --- a/src/agility/resources/knowledge_bases/knowledge_bases.py +++ b/src/agility/resources/knowledge_bases/knowledge_bases.py @@ -79,9 +79,7 @@ def create( Create a new knowledge base. Args: - ingestion_pipeline_params: Knowledge base pipeline params. - - Parameters defined on the knowledge-base level for a pipeline. + ingestion_pipeline_params: Knowledge base pipeline params input. extra_headers: Send extra headers @@ -307,9 +305,7 @@ async def create( Create a new knowledge base. Args: - ingestion_pipeline_params: Knowledge base pipeline params. - - Parameters defined on the knowledge-base level for a pipeline. + ingestion_pipeline_params: Knowledge base pipeline params input. extra_headers: Send extra headers diff --git a/src/agility/types/knowledge_base_create_params.py b/src/agility/types/knowledge_base_create_params.py index 36e547e..8503149 100644 --- a/src/agility/types/knowledge_base_create_params.py +++ b/src/agility/types/knowledge_base_create_params.py @@ -31,10 +31,7 @@ class KnowledgeBaseCreateParams(TypedDict, total=False): description: Required[str] ingestion_pipeline_params: Required[IngestionPipelineParams] - """Knowledge base pipeline params. - - Parameters defined on the knowledge-base level for a pipeline. - """ + """Knowledge base pipeline params input.""" name: Required[str] @@ -152,12 +149,6 @@ class IngestionPipelineParamsTransform(TypedDict, total=False): class IngestionPipelineParamsVectorStore(TypedDict, total=False): - weaviate_collection_name: Required[str] - """The name of the Weaviate collection to use for storing documents. - - Must start with AgilityKB and be valid. - """ - node_tags: Dict[str, str] @@ -180,4 +171,4 @@ class IngestionPipelineParams(TypedDict, total=False): """ vector_store: Required[IngestionPipelineParamsVectorStore] - """Vector store params.""" + """Vector store params input.""" diff --git a/tests/api_resources/test_knowledge_bases.py b/tests/api_resources/test_knowledge_bases.py index 2e915ba..1f7410f 100644 --- a/tests/api_resources/test_knowledge_bases.py +++ b/tests/api_resources/test_knowledge_bases.py @@ -29,7 +29,7 @@ def test_method_create(self, client: Agility) -> None: "curate": {}, "curate_document_store": {}, "transform": {}, - "vector_store": {"weaviate_collection_name": "weaviate_collection_name"}, + "vector_store": {}, }, name="name", ) @@ -43,7 +43,7 @@ def test_raw_response_create(self, client: Agility) -> None: "curate": {}, "curate_document_store": {}, "transform": {}, - "vector_store": {"weaviate_collection_name": "weaviate_collection_name"}, + "vector_store": {}, }, name="name", ) @@ -61,7 +61,7 @@ def test_streaming_response_create(self, client: Agility) -> None: "curate": {}, "curate_document_store": {}, "transform": {}, - "vector_store": {"weaviate_collection_name": "weaviate_collection_name"}, + "vector_store": {}, }, name="name", ) as response: @@ -264,7 +264,7 @@ async def test_method_create(self, async_client: AsyncAgility) -> None: "curate": {}, "curate_document_store": {}, "transform": {}, - "vector_store": {"weaviate_collection_name": "weaviate_collection_name"}, + "vector_store": {}, }, name="name", ) @@ -278,7 +278,7 @@ async def test_raw_response_create(self, async_client: AsyncAgility) -> None: "curate": {}, "curate_document_store": {}, "transform": {}, - "vector_store": {"weaviate_collection_name": "weaviate_collection_name"}, + "vector_store": {}, }, name="name", ) @@ -296,7 +296,7 @@ async def test_streaming_response_create(self, async_client: AsyncAgility) -> No "curate": {}, "curate_document_store": {}, "transform": {}, - "vector_store": {"weaviate_collection_name": "weaviate_collection_name"}, + "vector_store": {}, }, name="name", ) as response: From 94ac4a34d0363d03956afcdfa7edbac0af895560 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 5 Feb 2025 19:14:27 +0000 Subject: [PATCH 58/77] feat(api): api update --- .stats.yml | 2 +- src/agility/types/assistant_list_response.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 573f249..5d551ad 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 42 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-b8c06b2cd81f690ccbf027f4dcaf2af7597a200a3b8b58d0f53240f3b18c6bce.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-5bc5b86db5d9f1209c761082fedd3b510cffef5ee29b3fa00ddc176c4303812b.yml diff --git a/src/agility/types/assistant_list_response.py b/src/agility/types/assistant_list_response.py index c2d99b8..28b434d 100644 --- a/src/agility/types/assistant_list_response.py +++ b/src/agility/types/assistant_list_response.py @@ -34,7 +34,7 @@ class AssistantListResponse(BaseModel): knowledge_base_id: Optional[str] = None - knowledge_base_name: str + knowledge_base_name: Optional[str] = None name: str """The name of the assistant""" From d70b3314b519cbf153a4139a299d7836ee318694 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 6 Feb 2025 03:04:36 +0000 Subject: [PATCH 59/77] feat(client): send `X-Stainless-Read-Timeout` header --- src/agility/_base_client.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/agility/_base_client.py b/src/agility/_base_client.py index b8bea80..e5271fd 100644 --- a/src/agility/_base_client.py +++ b/src/agility/_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 1a8973521c873eee68d36eece08b9da5faa52e58 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 7 Feb 2025 03:02:37 +0000 Subject: [PATCH 60/77] chore(internal): fix type traversing dictionary params --- src/agility/_utils/_transform.py | 12 +++++++++++- tests/test_transform.py | 11 ++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/agility/_utils/_transform.py b/src/agility/_utils/_transform.py index a6b62ca..18afd9d 100644 --- a/src/agility/_utils/_transform.py +++ b/src/agility/_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 bdb468b..2f181cb 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 978943cf6575781d0529a5213afc11beeeb758e2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 7 Feb 2025 03:05:39 +0000 Subject: [PATCH 61/77] chore(internal): minor type handling changes --- src/agility/_models.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/agility/_models.py b/src/agility/_models.py index 12c34b7..c4401ff 100644 --- a/src/agility/_models.py +++ b/src/agility/_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 9f90e06a97763b770e95185a18000bccc79dd7eb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 13 Feb 2025 03:06:12 +0000 Subject: [PATCH 62/77] chore(internal): update client tests --- tests/test_client.py | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 5927f2d..768b790 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -23,6 +23,7 @@ from agility import Agility, AsyncAgility, APIResponseValidationError from agility._types import Omit +from agility._utils import maybe_transform from agility._models import BaseModel, FinalRequestOptions from agility._constants import RAW_RESPONSE_HEADER from agility._exceptions import APIStatusError, APITimeoutError, APIResponseValidationError @@ -32,6 +33,7 @@ BaseClient, make_request_options, ) +from agility.types.assistant_create_params import AssistantCreateParams from .utils import update_env @@ -715,8 +717,13 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> No "/api/assistants/", body=cast( object, - dict( - description="description", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", name="name" + maybe_transform( + dict( + description="description", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + ), + AssistantCreateParams, ), ), cast_to=httpx.Response, @@ -735,8 +742,13 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> Non "/api/assistants/", body=cast( object, - dict( - description="description", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", name="name" + maybe_transform( + dict( + description="description", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + ), + AssistantCreateParams, ), ), cast_to=httpx.Response, @@ -1513,8 +1525,13 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) "/api/assistants/", body=cast( object, - dict( - description="description", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", name="name" + maybe_transform( + dict( + description="description", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + ), + AssistantCreateParams, ), ), cast_to=httpx.Response, @@ -1533,8 +1550,13 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) "/api/assistants/", body=cast( object, - dict( - description="description", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", name="name" + maybe_transform( + dict( + description="description", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + ), + AssistantCreateParams, ), ), cast_to=httpx.Response, From 0c14b2c2168700538a16552d1e4ec5210aeea014 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 14 Feb 2025 03:01:25 +0000 Subject: [PATCH 63/77] fix: asyncify on non-asyncio runtimes --- src/agility/_utils/_sync.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/agility/_utils/_sync.py b/src/agility/_utils/_sync.py index 8b3aaf2..ad7ec71 100644 --- a/src/agility/_utils/_sync.py +++ b/src/agility/_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 9bcfa0edfc5ad295b36ce0f8916590e2e8d1a8f4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 21 Feb 2025 03:27:23 +0000 Subject: [PATCH 64/77] feat(client): allow passing `NotGiven` for body fix(client): mark some request bodies as optional --- src/agility/_base_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agility/_base_client.py b/src/agility/_base_client.py index e5271fd..3b3f7ce 100644 --- a/src/agility/_base_client.py +++ b/src/agility/_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 715627e1e157db7ae2901b11ea4ef4cceb5bb8ec Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 22 Feb 2025 03:08:04 +0000 Subject: [PATCH 65/77] chore(internal): fix devcontainers setup --- .devcontainer/Dockerfile | 2 +- .devcontainer/devcontainer.json | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index ac9a2e7..55d2025 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 bbeb30b..c17fdc1 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 ab2e09418a14095acaa81f21ff778452afd9c328 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 26 Feb 2025 03:01:12 +0000 Subject: [PATCH 66/77] chore(internal): properly set __pydantic_private__ --- src/agility/_base_client.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/agility/_base_client.py b/src/agility/_base_client.py index 3b3f7ce..b3c48b5 100644 --- a/src/agility/_base_client.py +++ b/src/agility/_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 4a8ec35bfd9e9bfca8be7c055fca7b96b1b29be7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 28 Feb 2025 03:01:32 +0000 Subject: [PATCH 67/77] docs: update URLs from stainlessapi.com to stainless.com More details at https://www.stainless.com/changelog/stainless-com --- README.md | 4 ++-- SECURITY.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 856c344..15b64eb 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ The Agility Python library provides convenient access to the Agility REST API fr 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 @@ -20,7 +20,7 @@ pip install git+ssh://git@github.com/stainless-sdks/agility-python.git ``` > [!NOTE] -> Once this package is [published to PyPI](https://app.stainlessapi.com/docs/guides/publish), this will become: `pip install --pre agility` +> Once this package is [published to PyPI](https://app.stainless.com/docs/guides/publish), this will become: `pip install --pre agility` ## Usage diff --git a/SECURITY.md b/SECURITY.md index c32c806..9c3e857 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 8531cea4bc4082aecb7ad4eaff1fab9e568e9a26 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 28 Feb 2025 03:02:08 +0000 Subject: [PATCH 68/77] chore(docs): update client docstring --- src/agility/_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/agility/_client.py b/src/agility/_client.py index 9fe8b29..1e38a69 100644 --- a/src/agility/_client.py +++ b/src/agility/_client.py @@ -99,7 +99,7 @@ def __init__( # part of our public interface in the future. _strict_response_validation: bool = False, ) -> None: - """Construct a new synchronous agility client instance. + """Construct a new synchronous Agility client instance. This automatically infers the following arguments from their corresponding environment variables if they are not provided: - `bearer_token` from `BEARER_TOKEN` @@ -344,7 +344,7 @@ def __init__( # part of our public interface in the future. _strict_response_validation: bool = False, ) -> None: - """Construct a new async agility client instance. + """Construct a new async AsyncAgility client instance. This automatically infers the following arguments from their corresponding environment variables if they are not provided: - `bearer_token` from `BEARER_TOKEN` From 6b4d48fc791c5b96aee648fbee839e62c9939166 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 4 Mar 2025 03:12:12 +0000 Subject: [PATCH 69/77] chore(internal): remove unused http client options forwarding --- src/agility/_base_client.py | 97 +------------------------------------ 1 file changed, 1 insertion(+), 96 deletions(-) diff --git a/src/agility/_base_client.py b/src/agility/_base_client.py index b3c48b5..caf5b3c 100644 --- a/src/agility/_base_client.py +++ b/src/agility/_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 8d3ce0b06ba94d8bb4a29063975ee3af38dff20d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 10 Apr 2025 17:16:06 +0000 Subject: [PATCH 70/77] feat(api): api update --- .stats.yml | 2 +- requirements-dev.lock | 1 + requirements.lock | 1 + .../resources/assistants/assistants.py | 32 +++++++++++++++++++ src/agility/types/assistant.py | 6 ++++ src/agility/types/assistant_create_params.py | 6 ++++ src/agility/types/assistant_list_response.py | 6 ++++ src/agility/types/assistant_update_params.py | 6 ++++ src/agility/types/assistant_with_config.py | 6 ++++ .../types/integration_create_response.py | 4 +-- .../types/integration_list_response.py | 4 +-- .../types/integration_retrieve_response.py | 4 +-- src/agility/types/s3_v0_integration.py | 4 +-- tests/api_resources/test_assistants.py | 8 +++++ 14 files changed, 81 insertions(+), 9 deletions(-) diff --git a/.stats.yml b/.stats.yml index 5d551ad..1552dcc 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 42 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-5bc5b86db5d9f1209c761082fedd3b510cffef5ee29b3fa00ddc176c4303812b.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-d95b89c34a94346fe256de0aea6f41a760399f8733a35226ad9aa0d89fdf4bdb.yml diff --git a/requirements-dev.lock b/requirements-dev.lock index 0d6651f..2ecab23 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 b2ab8b1..8f0aee0 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 diff --git a/src/agility/resources/assistants/assistants.py b/src/agility/resources/assistants/assistants.py index dc25263..9b3f4fb 100644 --- a/src/agility/resources/assistants/assistants.py +++ b/src/agility/resources/assistants/assistants.py @@ -70,6 +70,8 @@ def create( name: str, context_limit: Optional[int] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, + logo_s3_key: Optional[str] | NotGiven = NOT_GIVEN, + logo_text: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, suggested_questions: List[str] | NotGiven = NOT_GIVEN, tools: Optional[Iterable[assistant_create_params.Tool]] | NotGiven = NOT_GIVEN, @@ -91,6 +93,10 @@ def create( context_limit: The maximum number of context chunks to include in a run. + logo_s3_key: S3 object key to the assistant's logo image + + logo_text: Text to display alongside the assistant's logo + suggested_questions: A list of suggested questions that can be asked to the assistant url_slug: Optional URL suffix - unique identifier for the assistant's endpoint @@ -112,6 +118,8 @@ def create( "name": name, "context_limit": context_limit, "instructions": instructions, + "logo_s3_key": logo_s3_key, + "logo_text": logo_text, "model": model, "suggested_questions": suggested_questions, "tools": tools, @@ -168,6 +176,8 @@ def update( name: str, context_limit: Optional[int] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, + logo_s3_key: Optional[str] | NotGiven = NOT_GIVEN, + logo_text: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, suggested_questions: List[str] | NotGiven = NOT_GIVEN, tools: Optional[Iterable[assistant_update_params.Tool]] | NotGiven = NOT_GIVEN, @@ -189,6 +199,10 @@ def update( context_limit: The maximum number of context chunks to include in a run. + logo_s3_key: S3 object key to the assistant's logo image + + logo_text: Text to display alongside the assistant's logo + suggested_questions: A list of suggested questions that can be asked to the assistant url_slug: Optional URL suffix - unique identifier for the assistant's endpoint @@ -213,6 +227,8 @@ def update( "name": name, "context_limit": context_limit, "instructions": instructions, + "logo_s3_key": logo_s3_key, + "logo_text": logo_text, "model": model, "suggested_questions": suggested_questions, "tools": tools, @@ -336,6 +352,8 @@ async def create( name: str, context_limit: Optional[int] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, + logo_s3_key: Optional[str] | NotGiven = NOT_GIVEN, + logo_text: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, suggested_questions: List[str] | NotGiven = NOT_GIVEN, tools: Optional[Iterable[assistant_create_params.Tool]] | NotGiven = NOT_GIVEN, @@ -357,6 +375,10 @@ async def create( context_limit: The maximum number of context chunks to include in a run. + logo_s3_key: S3 object key to the assistant's logo image + + logo_text: Text to display alongside the assistant's logo + suggested_questions: A list of suggested questions that can be asked to the assistant url_slug: Optional URL suffix - unique identifier for the assistant's endpoint @@ -378,6 +400,8 @@ async def create( "name": name, "context_limit": context_limit, "instructions": instructions, + "logo_s3_key": logo_s3_key, + "logo_text": logo_text, "model": model, "suggested_questions": suggested_questions, "tools": tools, @@ -434,6 +458,8 @@ async def update( name: str, context_limit: Optional[int] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, + logo_s3_key: Optional[str] | NotGiven = NOT_GIVEN, + logo_text: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, suggested_questions: List[str] | NotGiven = NOT_GIVEN, tools: Optional[Iterable[assistant_update_params.Tool]] | NotGiven = NOT_GIVEN, @@ -455,6 +481,10 @@ async def update( context_limit: The maximum number of context chunks to include in a run. + logo_s3_key: S3 object key to the assistant's logo image + + logo_text: Text to display alongside the assistant's logo + suggested_questions: A list of suggested questions that can be asked to the assistant url_slug: Optional URL suffix - unique identifier for the assistant's endpoint @@ -479,6 +509,8 @@ async def update( "name": name, "context_limit": context_limit, "instructions": instructions, + "logo_s3_key": logo_s3_key, + "logo_text": logo_text, "model": model, "suggested_questions": suggested_questions, "tools": tools, diff --git a/src/agility/types/assistant.py b/src/agility/types/assistant.py index 52bf49d..118c9ef 100644 --- a/src/agility/types/assistant.py +++ b/src/agility/types/assistant.py @@ -23,6 +23,12 @@ class Assistant(BaseModel): updated_at: datetime + logo_s3_key: Optional[str] = None + """S3 object key to the assistant's logo image""" + + logo_text: Optional[str] = None + """Text to display alongside the assistant's logo""" + suggested_questions: Optional[List[str]] = None """A list of suggested questions that can be asked to the assistant""" diff --git a/src/agility/types/assistant_create_params.py b/src/agility/types/assistant_create_params.py index b26411b..db2017e 100644 --- a/src/agility/types/assistant_create_params.py +++ b/src/agility/types/assistant_create_params.py @@ -22,6 +22,12 @@ class AssistantCreateParams(TypedDict, total=False): instructions: Optional[str] + logo_s3_key: Optional[str] + """S3 object key to the assistant's logo image""" + + logo_text: Optional[str] + """Text to display alongside the assistant's logo""" + model: Optional[Literal["gpt-4o"]] suggested_questions: List[str] diff --git a/src/agility/types/assistant_list_response.py b/src/agility/types/assistant_list_response.py index 28b434d..249214d 100644 --- a/src/agility/types/assistant_list_response.py +++ b/src/agility/types/assistant_list_response.py @@ -46,6 +46,12 @@ class AssistantListResponse(BaseModel): instructions: Optional[str] = None + logo_s3_key: Optional[str] = None + """S3 object key to the assistant's logo image""" + + logo_text: Optional[str] = None + """Text to display alongside the assistant's logo""" + model: Optional[Literal["gpt-4o"]] = None suggested_questions: Optional[List[str]] = None diff --git a/src/agility/types/assistant_update_params.py b/src/agility/types/assistant_update_params.py index 8725c27..53b8f1c 100644 --- a/src/agility/types/assistant_update_params.py +++ b/src/agility/types/assistant_update_params.py @@ -24,6 +24,12 @@ class AssistantUpdateParams(TypedDict, total=False): instructions: Optional[str] + logo_s3_key: Optional[str] + """S3 object key to the assistant's logo image""" + + logo_text: Optional[str] + """Text to display alongside the assistant's logo""" + model: Optional[Literal["gpt-4o"]] suggested_questions: List[str] diff --git a/src/agility/types/assistant_with_config.py b/src/agility/types/assistant_with_config.py index a87998a..9307ffc 100644 --- a/src/agility/types/assistant_with_config.py +++ b/src/agility/types/assistant_with_config.py @@ -44,6 +44,12 @@ class AssistantWithConfig(BaseModel): instructions: Optional[str] = None + logo_s3_key: Optional[str] = None + """S3 object key to the assistant's logo image""" + + logo_text: Optional[str] = None + """Text to display alongside the assistant's logo""" + model: Optional[Literal["gpt-4o"]] = None suggested_questions: Optional[List[str]] = None diff --git a/src/agility/types/integration_create_response.py b/src/agility/types/integration_create_response.py index 91b0891..8a3054c 100644 --- a/src/agility/types/integration_create_response.py +++ b/src/agility/types/integration_create_response.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Union, Optional +from typing import Dict, Union, Optional from typing_extensions import Literal, TypeAlias from .._models import BaseModel @@ -21,7 +21,7 @@ class NotionV0IntegrationTokenNotionAccessToken(BaseModel): bot_id: str - owner: object + owner: Dict[str, object] workspace_id: str diff --git a/src/agility/types/integration_list_response.py b/src/agility/types/integration_list_response.py index 96b8e15..704a2f4 100644 --- a/src/agility/types/integration_list_response.py +++ b/src/agility/types/integration_list_response.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Union, Optional +from typing import Dict, Union, Optional from typing_extensions import Literal, TypeAlias from .._models import BaseModel @@ -21,7 +21,7 @@ class NotionV0IntegrationTokenNotionAccessToken(BaseModel): bot_id: str - owner: object + owner: Dict[str, object] workspace_id: str diff --git a/src/agility/types/integration_retrieve_response.py b/src/agility/types/integration_retrieve_response.py index aa3aef3..1524b3b 100644 --- a/src/agility/types/integration_retrieve_response.py +++ b/src/agility/types/integration_retrieve_response.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Union, Optional +from typing import Dict, Union, Optional from typing_extensions import Literal, TypeAlias from .._models import BaseModel @@ -21,7 +21,7 @@ class NotionV0IntegrationTokenNotionAccessToken(BaseModel): bot_id: str - owner: object + owner: Dict[str, object] workspace_id: str diff --git a/src/agility/types/s3_v0_integration.py b/src/agility/types/s3_v0_integration.py index e9b09f9..e129a8e 100644 --- a/src/agility/types/s3_v0_integration.py +++ b/src/agility/types/s3_v0_integration.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional +from typing import Dict, Optional from typing_extensions import Literal from .._models import BaseModel @@ -17,7 +17,7 @@ class ResourceAccessDefinitionResource(BaseModel): class ResourceAccessDefinition(BaseModel): - policy: object + policy: Dict[str, object] resource: ResourceAccessDefinitionResource diff --git a/tests/api_resources/test_assistants.py b/tests/api_resources/test_assistants.py index 72dce68..3a851ac 100644 --- a/tests/api_resources/test_assistants.py +++ b/tests/api_resources/test_assistants.py @@ -39,6 +39,8 @@ def test_method_create_with_all_params(self, client: Agility) -> None: name="name", context_limit=1, instructions="instructions", + logo_s3_key="logo_s3_key", + logo_text="logo_text", model="gpt-4o", suggested_questions=["string"], tools=[ @@ -138,6 +140,8 @@ def test_method_update_with_all_params(self, client: Agility) -> None: name="name", context_limit=1, instructions="instructions", + logo_s3_key="logo_s3_key", + logo_text="logo_text", model="gpt-4o", suggested_questions=["string"], tools=[ @@ -285,6 +289,8 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgility) - name="name", context_limit=1, instructions="instructions", + logo_s3_key="logo_s3_key", + logo_text="logo_text", model="gpt-4o", suggested_questions=["string"], tools=[ @@ -384,6 +390,8 @@ async def test_method_update_with_all_params(self, async_client: AsyncAgility) - name="name", context_limit=1, instructions="instructions", + logo_s3_key="logo_s3_key", + logo_text="logo_text", model="gpt-4o", suggested_questions=["string"], tools=[ From 09f834525ca1d09719f6780959ad523b1f6044cb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 11 Apr 2025 02:05:36 +0000 Subject: [PATCH 71/77] chore(internal): codegen related update --- .devcontainer/Dockerfile | 2 +- .github/workflows/ci.yml | 4 ++-- .stats.yml | 2 ++ README.md | 30 ++++++++++++++++++++++++++++++ bin/publish-pypi | 3 --- pyproject.toml | 4 +--- scripts/test | 2 ++ src/agility/_client.py | 16 ++-------------- src/agility/_models.py | 9 ++++++--- src/agility/_utils/_transform.py | 2 +- tests/test_client.py | 2 +- tests/test_models.py | 32 ++++++++++++++++++++++++++++++++ 12 files changed, 80 insertions(+), 28 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 55d2025..ff261ba 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 c8a8a4f..3b286e5 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/.stats.yml b/.stats.yml index 1552dcc..f6dc541 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,4 @@ configured_endpoints: 42 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-d95b89c34a94346fe256de0aea6f41a760399f8733a35226ad9aa0d89fdf4bdb.yml +openapi_spec_hash: 9f5ee3dde20784fd428e05b36ad349b5 +config_hash: 6d2156cfe279456cf3c35ba5c66be1c1 diff --git a/README.md b/README.md index 15b64eb..c67402b 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,36 @@ for assistant in first_page.items: # Remove `await` for non-async usage. ``` +## Nested params + +Nested parameters are dictionaries, typed using `TypedDict`, for example: + +```python +from agility import Agility + +client = Agility() + +knowledge_base_with_config = client.knowledge_bases.create( + description="description", + ingestion_pipeline_params={ + "curate": {"steps": {"foo": {"name": "remove_exact_duplicates.v0"}}}, + "curate_document_store": {"document_tags": {"foo": "string"}}, + "transform": { + "steps": { + "foo": { + "chunk_overlap": 0, + "chunk_size": 0, + "name": "splitters.recursive_character.v0", + } + } + }, + "vector_store": {"node_tags": {"foo": "string"}}, + }, + name="name", +) +print(knowledge_base_with_config.ingestion_pipeline_params) +``` + ## 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 `agility.APIConnectionError` is raised. diff --git a/bin/publish-pypi b/bin/publish-pypi index 05bfccb..826054e 100644 --- a/bin/publish-pypi +++ b/bin/publish-pypi @@ -3,7 +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 pip install 'importlib-metadata==7.2.1' rye publish --yes --token=$PYPI_TOKEN diff --git a/pyproject.toml b/pyproject.toml index 6d45dbd..7200bf2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,6 @@ Homepage = "https://github.com/stainless-sdks/agility-python" Repository = "https://github.com/stainless-sdks/agility-python" - [tool.rye] managed = true # version pins are in requirements-dev.lock @@ -87,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] @@ -152,7 +151,6 @@ reportImplicitOverride = true reportImportCycles = false reportPrivateUsage = false - [tool.ruff] line-length = 120 output-format = "grouped" diff --git a/scripts/test b/scripts/test index 4fa5698..2b87845 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 "$@" diff --git a/src/agility/_client.py b/src/agility/_client.py index 1e38a69..818130a 100644 --- a/src/agility/_client.py +++ b/src/agility/_client.py @@ -175,13 +175,7 @@ def qs(self) -> Querystring: @property @override def auth_headers(self) -> dict[str, str]: - if self._http_bearer: - return self._http_bearer - if self._authenticated_api_key: - return self._authenticated_api_key - if self._public_access_key: - return self._public_access_key - return {} + return {**self._http_bearer, **self._authenticated_api_key, **self._public_access_key} @property def _http_bearer(self) -> dict[str, str]: @@ -420,13 +414,7 @@ def qs(self) -> Querystring: @property @override def auth_headers(self) -> dict[str, str]: - if self._http_bearer: - return self._http_bearer - if self._authenticated_api_key: - return self._authenticated_api_key - if self._public_access_key: - return self._public_access_key - return {} + return {**self._http_bearer, **self._authenticated_api_key, **self._public_access_key} @property def _http_bearer(self) -> dict[str, str]: diff --git a/src/agility/_models.py b/src/agility/_models.py index c4401ff..3493571 100644 --- a/src/agility/_models.py +++ b/src/agility/_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 @@ -678,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/agility/_utils/_transform.py b/src/agility/_utils/_transform.py index 18afd9d..7ac2e17 100644 --- a/src/agility/_utils/_transform.py +++ b/src/agility/_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: diff --git a/tests/test_client.py b/tests/test_client.py index 768b790..68bc2b7 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1669,7 +1669,7 @@ def test_get_platform(self) -> None: import threading from agility._utils import asyncify - from agility._base_client import get_platform + from agility._base_client import get_platform async def test_main() -> None: result = await asyncify(get_platform)() diff --git a/tests/test_models.py b/tests/test_models.py index 557d4a3..51b9989 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 e9526e9f969425cbdaea8e8b22e54193e1e0640e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 11 Apr 2025 02:11:49 +0000 Subject: [PATCH 72/77] chore(internal): slight transform perf improvement --- src/agility/_utils/_transform.py | 22 ++++++++++++++++++++++ tests/test_transform.py | 12 ++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/agility/_utils/_transform.py b/src/agility/_utils/_transform.py index 7ac2e17..3ec6208 100644 --- a/src/agility/_utils/_transform.py +++ b/src/agility/_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 2f181cb..cfaa23c 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 2308950feeda36903ffc8b2393ff8e5047825cae Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 11 Apr 2025 02:22:30 +0000 Subject: [PATCH 73/77] chore(internal): expand CI branch coverage --- .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 3b286e5..53a3a09 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 2352695474dd0b842d150b3cccd37590f04c597e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 11 Apr 2025 02:26:11 +0000 Subject: [PATCH 74/77] 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 53a3a09..81f6dc2 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 b4a3b870243ae299e4c76cfc10a73880a91bbf17 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 12 Apr 2025 02:14:23 +0000 Subject: [PATCH 75/77] fix(perf): skip traversing types for NotGiven values --- src/agility/_utils/_transform.py | 11 +++++++++++ tests/test_transform.py | 9 ++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/agility/_utils/_transform.py b/src/agility/_utils/_transform.py index 3ec6208..3b2b8e0 100644 --- a/src/agility/_utils/_transform.py +++ b/src/agility/_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 cfaa23c..11784bf 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -8,7 +8,7 @@ import pytest -from agility._types import Base64FileInput +from agility._types import NOT_GIVEN, Base64FileInput from agility._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 b2d6900ef2d57479e0aedca8405a3df9dcf170b5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 12 Apr 2025 02:15:16 +0000 Subject: [PATCH 76/77] fix(perf): optimize some hot paths --- src/agility/_utils/_transform.py | 14 +++++++++++++- src/agility/_utils/_typing.py | 2 ++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/agility/_utils/_transform.py b/src/agility/_utils/_transform.py index 3b2b8e0..b0cc20a 100644 --- a/src/agility/_utils/_transform.py +++ b/src/agility/_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/agility/_utils/_typing.py b/src/agility/_utils/_typing.py index 278749b..1958820 100644 --- a/src/agility/_utils/_typing.py +++ b/src/agility/_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])) From 15fbe099a36c47d1c59c0a32f3d82ac8f813dde7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 14 Apr 2025 20:09:04 +0000 Subject: [PATCH 77/77] feat(api): api update --- .stats.yml | 4 +-- .../resources/assistants/assistants.py | 8 ++++++ src/agility/resources/threads/runs.py | 8 ++++++ src/agility/types/assistant_create_params.py | 2 ++ src/agility/types/assistant_list_response.py | 2 ++ src/agility/types/assistant_update_params.py | 2 ++ src/agility/types/assistant_with_config.py | 2 ++ src/agility/types/threads/message.py | 16 +++++++++-- .../types/threads/message_create_params.py | 16 +++++++++-- src/agility/types/threads/run.py | 2 ++ .../types/threads/run_create_params.py | 17 ++++++++++- .../types/threads/run_stream_params.py | 17 ++++++++++- tests/api_resources/test_assistants.py | 4 +++ tests/api_resources/threads/test_messages.py | 12 ++++++++ tests/api_resources/threads/test_runs.py | 28 +++++++++++++++++++ 15 files changed, 132 insertions(+), 8 deletions(-) diff --git a/.stats.yml b/.stats.yml index f6dc541..bb3959b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 42 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-d95b89c34a94346fe256de0aea6f41a760399f8733a35226ad9aa0d89fdf4bdb.yml -openapi_spec_hash: 9f5ee3dde20784fd428e05b36ad349b5 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-b890eea39144ced38d6196197401b71ac734bcd2343d046c04169bfab9d746f2.yml +openapi_spec_hash: 50035b732d759a3bc9a00402b5ccf5c4 config_hash: 6d2156cfe279456cf3c35ba5c66be1c1 diff --git a/src/agility/resources/assistants/assistants.py b/src/agility/resources/assistants/assistants.py index 9b3f4fb..232caa8 100644 --- a/src/agility/resources/assistants/assistants.py +++ b/src/agility/resources/assistants/assistants.py @@ -68,6 +68,7 @@ def create( description: str, knowledge_base_id: Optional[str], name: str, + codex_access_key: Optional[str] | NotGiven = NOT_GIVEN, context_limit: Optional[int] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, logo_s3_key: Optional[str] | NotGiven = NOT_GIVEN, @@ -116,6 +117,7 @@ def create( "description": description, "knowledge_base_id": knowledge_base_id, "name": name, + "codex_access_key": codex_access_key, "context_limit": context_limit, "instructions": instructions, "logo_s3_key": logo_s3_key, @@ -174,6 +176,7 @@ def update( description: str, knowledge_base_id: Optional[str], name: str, + codex_access_key: Optional[str] | NotGiven = NOT_GIVEN, context_limit: Optional[int] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, logo_s3_key: Optional[str] | NotGiven = NOT_GIVEN, @@ -225,6 +228,7 @@ def update( "description": description, "knowledge_base_id": knowledge_base_id, "name": name, + "codex_access_key": codex_access_key, "context_limit": context_limit, "instructions": instructions, "logo_s3_key": logo_s3_key, @@ -350,6 +354,7 @@ async def create( description: str, knowledge_base_id: Optional[str], name: str, + codex_access_key: Optional[str] | NotGiven = NOT_GIVEN, context_limit: Optional[int] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, logo_s3_key: Optional[str] | NotGiven = NOT_GIVEN, @@ -398,6 +403,7 @@ async def create( "description": description, "knowledge_base_id": knowledge_base_id, "name": name, + "codex_access_key": codex_access_key, "context_limit": context_limit, "instructions": instructions, "logo_s3_key": logo_s3_key, @@ -456,6 +462,7 @@ async def update( description: str, knowledge_base_id: Optional[str], name: str, + codex_access_key: Optional[str] | NotGiven = NOT_GIVEN, context_limit: Optional[int] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, logo_s3_key: Optional[str] | NotGiven = NOT_GIVEN, @@ -507,6 +514,7 @@ async def update( "description": description, "knowledge_base_id": knowledge_base_id, "name": name, + "codex_access_key": codex_access_key, "context_limit": context_limit, "instructions": instructions, "logo_s3_key": logo_s3_key, diff --git a/src/agility/resources/threads/runs.py b/src/agility/resources/threads/runs.py index f515e92..a55809a 100644 --- a/src/agility/resources/threads/runs.py +++ b/src/agility/resources/threads/runs.py @@ -54,6 +54,7 @@ def create( assistant_id: str, additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, additional_messages: Iterable[run_create_params.AdditionalMessage] | NotGiven = NOT_GIVEN, + codex_access_key: Optional[str] | NotGiven = NOT_GIVEN, context_limit: Optional[int] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, knowledge_base_id: Optional[str] | NotGiven = NOT_GIVEN, @@ -89,6 +90,7 @@ def create( "assistant_id": assistant_id, "additional_instructions": additional_instructions, "additional_messages": additional_messages, + "codex_access_key": codex_access_key, "context_limit": context_limit, "instructions": instructions, "knowledge_base_id": knowledge_base_id, @@ -183,6 +185,7 @@ def stream( assistant_id: str, additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, additional_messages: Iterable[run_stream_params.AdditionalMessage] | NotGiven = NOT_GIVEN, + codex_access_key: Optional[str] | NotGiven = NOT_GIVEN, context_limit: Optional[int] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, knowledge_base_id: Optional[str] | NotGiven = NOT_GIVEN, @@ -218,6 +221,7 @@ def stream( "assistant_id": assistant_id, "additional_instructions": additional_instructions, "additional_messages": additional_messages, + "codex_access_key": codex_access_key, "context_limit": context_limit, "instructions": instructions, "knowledge_base_id": knowledge_base_id, @@ -260,6 +264,7 @@ async def create( assistant_id: str, additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, additional_messages: Iterable[run_create_params.AdditionalMessage] | NotGiven = NOT_GIVEN, + codex_access_key: Optional[str] | NotGiven = NOT_GIVEN, context_limit: Optional[int] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, knowledge_base_id: Optional[str] | NotGiven = NOT_GIVEN, @@ -295,6 +300,7 @@ async def create( "assistant_id": assistant_id, "additional_instructions": additional_instructions, "additional_messages": additional_messages, + "codex_access_key": codex_access_key, "context_limit": context_limit, "instructions": instructions, "knowledge_base_id": knowledge_base_id, @@ -389,6 +395,7 @@ async def stream( assistant_id: str, additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, additional_messages: Iterable[run_stream_params.AdditionalMessage] | NotGiven = NOT_GIVEN, + codex_access_key: Optional[str] | NotGiven = NOT_GIVEN, context_limit: Optional[int] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, knowledge_base_id: Optional[str] | NotGiven = NOT_GIVEN, @@ -424,6 +431,7 @@ async def stream( "assistant_id": assistant_id, "additional_instructions": additional_instructions, "additional_messages": additional_messages, + "codex_access_key": codex_access_key, "context_limit": context_limit, "instructions": instructions, "knowledge_base_id": knowledge_base_id, diff --git a/src/agility/types/assistant_create_params.py b/src/agility/types/assistant_create_params.py index db2017e..1bc8eec 100644 --- a/src/agility/types/assistant_create_params.py +++ b/src/agility/types/assistant_create_params.py @@ -17,6 +17,8 @@ class AssistantCreateParams(TypedDict, total=False): name: Required[str] """The name of the assistant""" + codex_access_key: Optional[str] + context_limit: Optional[int] """The maximum number of context chunks to include in a run.""" diff --git a/src/agility/types/assistant_list_response.py b/src/agility/types/assistant_list_response.py index 249214d..5ccc27a 100644 --- a/src/agility/types/assistant_list_response.py +++ b/src/agility/types/assistant_list_response.py @@ -41,6 +41,8 @@ class AssistantListResponse(BaseModel): updated_at: datetime + codex_access_key: Optional[str] = None + context_limit: Optional[int] = None """The maximum number of context chunks to include in a run.""" diff --git a/src/agility/types/assistant_update_params.py b/src/agility/types/assistant_update_params.py index 53b8f1c..a97c2db 100644 --- a/src/agility/types/assistant_update_params.py +++ b/src/agility/types/assistant_update_params.py @@ -19,6 +19,8 @@ class AssistantUpdateParams(TypedDict, total=False): name: Required[str] """The name of the assistant""" + codex_access_key: Optional[str] + context_limit: Optional[int] """The maximum number of context chunks to include in a run.""" diff --git a/src/agility/types/assistant_with_config.py b/src/agility/types/assistant_with_config.py index 9307ffc..fd3a213 100644 --- a/src/agility/types/assistant_with_config.py +++ b/src/agility/types/assistant_with_config.py @@ -39,6 +39,8 @@ class AssistantWithConfig(BaseModel): updated_at: datetime + codex_access_key: Optional[str] = None + context_limit: Optional[int] = None """The maximum number of context chunks to include in a run.""" diff --git a/src/agility/types/threads/message.py b/src/agility/types/threads/message.py index f36e609..45aa481 100644 --- a/src/agility/types/threads/message.py +++ b/src/agility/types/threads/message.py @@ -1,17 +1,29 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List, Optional +from typing import Dict, List, Optional from datetime import datetime from typing_extensions import Literal from ..._models import BaseModel -__all__ = ["Message", "Metadata"] +__all__ = ["Message", "Metadata", "MetadataScores"] + + +class MetadataScores(BaseModel): + response_helpfulness: Optional[Dict[str, object]] = None + + trustworthiness: Optional[Dict[str, object]] = None class Metadata(BaseModel): citations: Optional[List[str]] = None + is_bad_response: Optional[bool] = None + + is_expert_answer: Optional[bool] = None + + scores: Optional[MetadataScores] = None + trustworthiness_explanation: Optional[str] = None trustworthiness_score: Optional[float] = None diff --git a/src/agility/types/threads/message_create_params.py b/src/agility/types/threads/message_create_params.py index 9048755..a11ef3c 100644 --- a/src/agility/types/threads/message_create_params.py +++ b/src/agility/types/threads/message_create_params.py @@ -2,10 +2,10 @@ from __future__ import annotations -from typing import List, Optional +from typing import Dict, List, Optional from typing_extensions import Literal, Required, TypedDict -__all__ = ["MessageCreateParams", "Metadata"] +__all__ = ["MessageCreateParams", "Metadata", "MetadataScores"] class MessageCreateParams(TypedDict, total=False): @@ -16,9 +16,21 @@ class MessageCreateParams(TypedDict, total=False): role: Required[Literal["user", "assistant"]] +class MetadataScores(TypedDict, total=False): + response_helpfulness: Optional[Dict[str, object]] + + trustworthiness: Optional[Dict[str, object]] + + class Metadata(TypedDict, total=False): citations: Optional[List[str]] + is_bad_response: Optional[bool] + + is_expert_answer: Optional[bool] + + scores: Optional[MetadataScores] + trustworthiness_explanation: Optional[str] trustworthiness_score: Optional[float] diff --git a/src/agility/types/threads/run.py b/src/agility/types/threads/run.py index 097e798..4d0e03d 100644 --- a/src/agility/types/threads/run.py +++ b/src/agility/types/threads/run.py @@ -45,6 +45,8 @@ class Run(BaseModel): additional_instructions: Optional[str] = None + codex_access_key: Optional[str] = None + context_limit: Optional[int] = None """The maximum number of context chunks to include.""" diff --git a/src/agility/types/threads/run_create_params.py b/src/agility/types/threads/run_create_params.py index 7bd31b2..13295cf 100644 --- a/src/agility/types/threads/run_create_params.py +++ b/src/agility/types/threads/run_create_params.py @@ -2,13 +2,14 @@ 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__ = [ "RunCreateParams", "AdditionalMessage", "AdditionalMessageMetadata", + "AdditionalMessageMetadataScores", "Tool", "ToolCodexV0Tool", "ToolNoOpTool", @@ -22,6 +23,8 @@ class RunCreateParams(TypedDict, total=False): additional_messages: Iterable[AdditionalMessage] + codex_access_key: Optional[str] + context_limit: Optional[int] """The maximum number of context chunks to include.""" @@ -34,9 +37,21 @@ class RunCreateParams(TypedDict, total=False): tools: Optional[Iterable[Tool]] +class AdditionalMessageMetadataScores(TypedDict, total=False): + response_helpfulness: Optional[Dict[str, object]] + + trustworthiness: Optional[Dict[str, object]] + + class AdditionalMessageMetadata(TypedDict, total=False): citations: Optional[List[str]] + is_bad_response: Optional[bool] + + is_expert_answer: Optional[bool] + + scores: Optional[AdditionalMessageMetadataScores] + trustworthiness_explanation: Optional[str] trustworthiness_score: Optional[float] diff --git a/src/agility/types/threads/run_stream_params.py b/src/agility/types/threads/run_stream_params.py index e13d49b..73171b0 100644 --- a/src/agility/types/threads/run_stream_params.py +++ b/src/agility/types/threads/run_stream_params.py @@ -2,13 +2,14 @@ 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__ = [ "RunStreamParams", "AdditionalMessage", "AdditionalMessageMetadata", + "AdditionalMessageMetadataScores", "Tool", "ToolCodexV0Tool", "ToolNoOpTool", @@ -22,6 +23,8 @@ class RunStreamParams(TypedDict, total=False): additional_messages: Iterable[AdditionalMessage] + codex_access_key: Optional[str] + context_limit: Optional[int] """The maximum number of context chunks to include.""" @@ -34,9 +37,21 @@ class RunStreamParams(TypedDict, total=False): tools: Optional[Iterable[Tool]] +class AdditionalMessageMetadataScores(TypedDict, total=False): + response_helpfulness: Optional[Dict[str, object]] + + trustworthiness: Optional[Dict[str, object]] + + class AdditionalMessageMetadata(TypedDict, total=False): citations: Optional[List[str]] + is_bad_response: Optional[bool] + + is_expert_answer: Optional[bool] + + scores: Optional[AdditionalMessageMetadataScores] + trustworthiness_explanation: Optional[str] trustworthiness_score: Optional[float] diff --git a/tests/api_resources/test_assistants.py b/tests/api_resources/test_assistants.py index 3a851ac..c0cf764 100644 --- a/tests/api_resources/test_assistants.py +++ b/tests/api_resources/test_assistants.py @@ -37,6 +37,7 @@ def test_method_create_with_all_params(self, client: Agility) -> None: description="description", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", name="name", + codex_access_key="codex_access_key", context_limit=1, instructions="instructions", logo_s3_key="logo_s3_key", @@ -138,6 +139,7 @@ def test_method_update_with_all_params(self, client: Agility) -> None: description="description", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", name="name", + codex_access_key="codex_access_key", context_limit=1, instructions="instructions", logo_s3_key="logo_s3_key", @@ -287,6 +289,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgility) - description="description", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", name="name", + codex_access_key="codex_access_key", context_limit=1, instructions="instructions", logo_s3_key="logo_s3_key", @@ -388,6 +391,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncAgility) - description="description", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", name="name", + codex_access_key="codex_access_key", context_limit=1, instructions="instructions", logo_s3_key="logo_s3_key", diff --git a/tests/api_resources/threads/test_messages.py b/tests/api_resources/threads/test_messages.py index 2751f99..f65ed3d 100644 --- a/tests/api_resources/threads/test_messages.py +++ b/tests/api_resources/threads/test_messages.py @@ -35,6 +35,12 @@ def test_method_create_with_all_params(self, client: Agility) -> None: content="content", metadata={ "citations": ["string"], + "is_bad_response": True, + "is_expert_answer": True, + "scores": { + "response_helpfulness": {"foo": "bar"}, + "trustworthiness": {"foo": "bar"}, + }, "trustworthiness_explanation": "trustworthiness_explanation", "trustworthiness_score": 0, }, @@ -246,6 +252,12 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgility) - content="content", metadata={ "citations": ["string"], + "is_bad_response": True, + "is_expert_answer": True, + "scores": { + "response_helpfulness": {"foo": "bar"}, + "trustworthiness": {"foo": "bar"}, + }, "trustworthiness_explanation": "trustworthiness_explanation", "trustworthiness_score": 0, }, diff --git a/tests/api_resources/threads/test_runs.py b/tests/api_resources/threads/test_runs.py index 3b17a5f..af3e153 100644 --- a/tests/api_resources/threads/test_runs.py +++ b/tests/api_resources/threads/test_runs.py @@ -36,6 +36,12 @@ def test_method_create_with_all_params(self, client: Agility) -> None: "content": "content", "metadata": { "citations": ["string"], + "is_bad_response": True, + "is_expert_answer": True, + "scores": { + "response_helpfulness": {"foo": "bar"}, + "trustworthiness": {"foo": "bar"}, + }, "trustworthiness_explanation": "trustworthiness_explanation", "trustworthiness_score": 0, }, @@ -43,6 +49,7 @@ def test_method_create_with_all_params(self, client: Agility) -> None: "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", } ], + codex_access_key="codex_access_key", context_limit=1, instructions="instructions", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", @@ -205,6 +212,12 @@ def test_method_stream_with_all_params(self, client: Agility) -> None: "content": "content", "metadata": { "citations": ["string"], + "is_bad_response": True, + "is_expert_answer": True, + "scores": { + "response_helpfulness": {"foo": "bar"}, + "trustworthiness": {"foo": "bar"}, + }, "trustworthiness_explanation": "trustworthiness_explanation", "trustworthiness_score": 0, }, @@ -212,6 +225,7 @@ def test_method_stream_with_all_params(self, client: Agility) -> None: "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", } ], + codex_access_key="codex_access_key", context_limit=1, instructions="instructions", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", @@ -282,6 +296,12 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgility) - "content": "content", "metadata": { "citations": ["string"], + "is_bad_response": True, + "is_expert_answer": True, + "scores": { + "response_helpfulness": {"foo": "bar"}, + "trustworthiness": {"foo": "bar"}, + }, "trustworthiness_explanation": "trustworthiness_explanation", "trustworthiness_score": 0, }, @@ -289,6 +309,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgility) - "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", } ], + codex_access_key="codex_access_key", context_limit=1, instructions="instructions", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", @@ -451,6 +472,12 @@ async def test_method_stream_with_all_params(self, async_client: AsyncAgility) - "content": "content", "metadata": { "citations": ["string"], + "is_bad_response": True, + "is_expert_answer": True, + "scores": { + "response_helpfulness": {"foo": "bar"}, + "trustworthiness": {"foo": "bar"}, + }, "trustworthiness_explanation": "trustworthiness_explanation", "trustworthiness_score": 0, }, @@ -458,6 +485,7 @@ async def test_method_stream_with_all_params(self, async_client: AsyncAgility) - "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", } ], + codex_access_key="codex_access_key", context_limit=1, instructions="instructions", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",