├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .github └── workflows │ ├── ci.yml │ ├── publish-pypi.yml │ └── release-doctor.yml ├── .gitignore ├── .python-version ├── .release-please-manifest.json ├── .stats.yml ├── Brewfile ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── api.md ├── bin ├── check-release-environment └── publish-pypi ├── examples └── .keep ├── mypy.ini ├── noxfile.py ├── pyproject.toml ├── release-please-config.json ├── requirements-dev.lock ├── requirements.lock ├── scripts ├── bootstrap ├── format ├── lint ├── mock ├── test └── utils │ ├── ruffen-docs.py │ └── upload-artifact.sh ├── src ├── retell │ ├── __init__.py │ ├── _base_client.py │ ├── _client.py │ ├── _compat.py │ ├── _constants.py │ ├── _exceptions.py │ ├── _files.py │ ├── _models.py │ ├── _qs.py │ ├── _resource.py │ ├── _response.py │ ├── _streaming.py │ ├── _types.py │ ├── _utils │ │ ├── __init__.py │ │ ├── _logs.py │ │ ├── _proxy.py │ │ ├── _reflection.py │ │ ├── _resources_proxy.py │ │ ├── _streams.py │ │ ├── _sync.py │ │ ├── _transform.py │ │ ├── _typing.py │ │ └── _utils.py │ ├── _version.py │ ├── lib │ │ ├── .keep │ │ ├── __init__.py │ │ └── webhook_auth.py │ ├── py.typed │ ├── resources │ │ ├── __init__.py │ │ ├── agent.py │ │ ├── batch_call.py │ │ ├── call.py │ │ ├── chat.py │ │ ├── concurrency.py │ │ ├── knowledge_base.py │ │ ├── llm.py │ │ ├── phone_number.py │ │ └── voice.py │ └── types │ │ ├── __init__.py │ │ ├── agent_create_params.py │ │ ├── agent_get_versions_response.py │ │ ├── agent_list_response.py │ │ ├── agent_response.py │ │ ├── agent_retrieve_params.py │ │ ├── agent_update_params.py │ │ ├── batch_call_create_batch_call_params.py │ │ ├── batch_call_response.py │ │ ├── call_create_phone_call_params.py │ │ ├── call_create_web_call_params.py │ │ ├── call_list_params.py │ │ ├── call_list_response.py │ │ ├── call_register_phone_call_params.py │ │ ├── call_response.py │ │ ├── call_update_params.py │ │ ├── chat_create_chat_completion_params.py │ │ ├── chat_create_chat_completion_response.py │ │ ├── chat_create_params.py │ │ ├── chat_list_response.py │ │ ├── chat_response.py │ │ ├── concurrency_retrieve_response.py │ │ ├── knowledge_base_add_sources_params.py │ │ ├── knowledge_base_create_params.py │ │ ├── knowledge_base_list_response.py │ │ ├── knowledge_base_response.py │ │ ├── llm_create_params.py │ │ ├── llm_list_response.py │ │ ├── llm_response.py │ │ ├── llm_retrieve_params.py │ │ ├── llm_update_params.py │ │ ├── phone_call_response.py │ │ ├── phone_number_create_params.py │ │ ├── phone_number_import_params.py │ │ ├── phone_number_list_response.py │ │ ├── phone_number_response.py │ │ ├── phone_number_update_params.py │ │ ├── voice_list_response.py │ │ ├── voice_response.py │ │ └── web_call_response.py ├── retell_ai │ └── lib │ │ └── .keep ├── retell_sdk │ └── lib │ │ └── .keep └── toddlzt │ └── lib │ └── .keep └── tests ├── __init__.py ├── api_resources ├── __init__.py ├── test_agent.py ├── test_batch_call.py ├── test_call.py ├── test_chat.py ├── test_concurrency.py ├── test_knowledge_base.py ├── test_llm.py ├── test_phone_number.py └── test_voice.py ├── conftest.py ├── sample_file.txt ├── test_client.py ├── test_deepcopy.py ├── test_extract_files.py ├── test_files.py ├── test_models.py ├── test_qs.py ├── test_required_args.py ├── test_response.py ├── test_streaming.py ├── test_transform.py ├── test_utils ├── test_proxy.py └── test_typing.py ├── utils.py └── webhook_auth.py /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG VARIANT="3.9" 2 | FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} 3 | 4 | USER vscode 5 | 6 | RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.44.0" RYE_INSTALL_OPTION="--yes" bash 7 | ENV PATH=/home/vscode/.rye/shims:$PATH 8 | 9 | RUN echo "[[ -d .venv ]] && source .venv/bin/activate || export PATH=\$PATH" >> /home/vscode/.bashrc 10 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/debian 3 | { 4 | "name": "Debian", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | "context": ".." 8 | }, 9 | 10 | "postStartCommand": "rye sync --all-features", 11 | 12 | "customizations": { 13 | "vscode": { 14 | "extensions": [ 15 | "ms-python.python" 16 | ], 17 | "settings": { 18 | "terminal.integrated.shell.linux": "/bin/bash", 19 | "python.pythonPath": ".venv/bin/python", 20 | "python.defaultInterpreterPath": ".venv/bin/python", 21 | "python.typeChecking": "basic", 22 | "terminal.integrated.env.linux": { 23 | "PATH": "/home/vscode/.rye/shims:${env:PATH}" 24 | } 25 | } 26 | } 27 | }, 28 | "features": { 29 | "ghcr.io/devcontainers/features/node:1": {} 30 | } 31 | 32 | // Features to add to the dev container. More info: https://containers.dev/features. 33 | // "features": {}, 34 | 35 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 36 | // "forwardPorts": [], 37 | 38 | // Configure tool-specific properties. 39 | // "customizations": {}, 40 | 41 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 42 | // "remoteUser": "root" 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches-ignore: 5 | - 'generated' 6 | - 'codegen/**' 7 | - 'integrated/**' 8 | - 'stl-preview-head/**' 9 | - 'stl-preview-base/**' 10 | 11 | jobs: 12 | lint: 13 | timeout-minutes: 10 14 | name: lint 15 | runs-on: ${{ github.repository == 'stainless-sdks/toddlzt-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Install Rye 20 | run: | 21 | curl -sSf https://rye.astral.sh/get | bash 22 | echo "$HOME/.rye/shims" >> $GITHUB_PATH 23 | env: 24 | RYE_VERSION: '0.44.0' 25 | RYE_INSTALL_OPTION: '--yes' 26 | 27 | - name: Install dependencies 28 | run: rye sync --all-features 29 | 30 | - name: Run lints 31 | run: ./scripts/lint 32 | 33 | upload: 34 | if: github.repository == 'stainless-sdks/toddlzt-python' 35 | timeout-minutes: 10 36 | name: upload 37 | permissions: 38 | contents: read 39 | id-token: write 40 | runs-on: depot-ubuntu-24.04 41 | steps: 42 | - uses: actions/checkout@v4 43 | 44 | - name: Get GitHub OIDC Token 45 | id: github-oidc 46 | uses: actions/github-script@v6 47 | with: 48 | script: core.setOutput('github_token', await core.getIDToken()); 49 | 50 | - name: Upload tarball 51 | env: 52 | URL: https://pkg.stainless.com/s 53 | AUTH: ${{ steps.github-oidc.outputs.github_token }} 54 | SHA: ${{ github.sha }} 55 | run: ./scripts/utils/upload-artifact.sh 56 | 57 | test: 58 | timeout-minutes: 10 59 | name: test 60 | runs-on: ${{ github.repository == 'stainless-sdks/toddlzt-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} 61 | steps: 62 | - uses: actions/checkout@v4 63 | 64 | - name: Install Rye 65 | run: | 66 | curl -sSf https://rye.astral.sh/get | bash 67 | echo "$HOME/.rye/shims" >> $GITHUB_PATH 68 | env: 69 | RYE_VERSION: '0.44.0' 70 | RYE_INSTALL_OPTION: '--yes' 71 | 72 | - name: Bootstrap 73 | run: ./scripts/bootstrap 74 | 75 | - name: Run tests 76 | run: ./scripts/test 77 | -------------------------------------------------------------------------------- /.github/workflows/publish-pypi.yml: -------------------------------------------------------------------------------- 1 | # This workflow is triggered when a GitHub release is created. 2 | # It can also be run manually to re-publish to PyPI in case it failed for some reason. 3 | # You can run this workflow by navigating to https://www.github.com/RetellAI/retell-python-sdk/actions/workflows/publish-pypi.yml 4 | name: Publish PyPI 5 | on: 6 | workflow_dispatch: 7 | 8 | release: 9 | types: [published] 10 | 11 | jobs: 12 | publish: 13 | name: publish 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Install Rye 20 | run: | 21 | curl -sSf https://rye.astral.sh/get | bash 22 | echo "$HOME/.rye/shims" >> $GITHUB_PATH 23 | env: 24 | RYE_VERSION: '0.44.0' 25 | RYE_INSTALL_OPTION: '--yes' 26 | 27 | - name: Publish to PyPI 28 | run: | 29 | bash ./bin/publish-pypi 30 | env: 31 | PYPI_TOKEN: ${{ secrets.RETELL_PYPI_TOKEN || secrets.PYPI_TOKEN }} 32 | -------------------------------------------------------------------------------- /.github/workflows/release-doctor.yml: -------------------------------------------------------------------------------- 1 | name: Release Doctor 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | workflow_dispatch: 7 | 8 | jobs: 9 | release_doctor: 10 | name: release doctor 11 | runs-on: ubuntu-latest 12 | if: github.repository == 'RetellAI/retell-python-sdk' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Check release environment 18 | run: | 19 | bash ./bin/check-release-environment 20 | env: 21 | PYPI_TOKEN: ${{ secrets.RETELL_PYPI_TOKEN || secrets.PYPI_TOKEN }} 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .prism.log 2 | .vscode 3 | _dev 4 | 5 | __pycache__ 6 | .mypy_cache 7 | 8 | dist 9 | 10 | .venv 11 | .idea 12 | 13 | .env 14 | .envrc 15 | codegen.log 16 | Brewfile.lock.json 17 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.9.18 2 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "4.33.0" 3 | } -------------------------------------------------------------------------------- /.stats.yml: -------------------------------------------------------------------------------- 1 | configured_endpoints: 39 2 | openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/retell%2Ftoddlzt-6f9abd242d6ae2ff1402a11e59ee5c26fcac97bb4a3780fb9e12096d542c6b61.yml 3 | openapi_spec_hash: f5995d0f2a7ce0b2a88777b75dd6823f 4 | config_hash: f4bc63f2350a2a4988750b41a0737f9d 5 | -------------------------------------------------------------------------------- /Brewfile: -------------------------------------------------------------------------------- 1 | brew "rye" 2 | 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Setting up the environment 2 | 3 | ### With Rye 4 | 5 | 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: 6 | 7 | ```sh 8 | $ ./scripts/bootstrap 9 | ``` 10 | 11 | Or [install Rye manually](https://rye.astral.sh/guide/installation/) and run: 12 | 13 | ```sh 14 | $ rye sync --all-features 15 | ``` 16 | 17 | You can then run scripts using `rye run python script.py` or by activating the virtual environment: 18 | 19 | ```sh 20 | # Activate the virtual environment - https://docs.python.org/3/library/venv.html#how-venvs-work 21 | $ source .venv/bin/activate 22 | 23 | # now you can omit the `rye run` prefix 24 | $ python script.py 25 | ``` 26 | 27 | ### Without Rye 28 | 29 | 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: 30 | 31 | ```sh 32 | $ pip install -r requirements-dev.lock 33 | ``` 34 | 35 | ## Modifying/Adding code 36 | 37 | Most of the SDK is generated code. Modifications to code will be persisted between generations, but may 38 | result in merge conflicts between manual patches and changes from the generator. The generator will never 39 | modify the contents of the `src/retell/lib/` and `examples/` directories. 40 | 41 | ## Adding and running examples 42 | 43 | All files in the `examples/` directory are not modified by the generator and can be freely edited or added to. 44 | 45 | ```py 46 | # add an example to examples/.py 47 | 48 | #!/usr/bin/env -S rye run python 49 | … 50 | ``` 51 | 52 | ```sh 53 | $ chmod +x examples/.py 54 | # run the example against your api 55 | $ ./examples/.py 56 | ``` 57 | 58 | ## Using the repository from source 59 | 60 | If you’d like to use the repository from source, you can either install from git or link to a cloned repository: 61 | 62 | To install via git: 63 | 64 | ```sh 65 | $ pip install git+ssh://git@github.com/RetellAI/retell-python-sdk#main.git 66 | ``` 67 | 68 | Alternatively, you can build from source and install the wheel file: 69 | 70 | 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. 71 | 72 | To create a distributable version of the library, all you have to do is run this command: 73 | 74 | ```sh 75 | $ rye build 76 | # or 77 | $ python -m build 78 | ``` 79 | 80 | Then to install: 81 | 82 | ```sh 83 | $ pip install ./path-to-wheel-file.whl 84 | ``` 85 | 86 | ## Running tests 87 | 88 | Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests. 89 | 90 | ```sh 91 | # you will need npm installed 92 | $ npx prism mock path/to/your/openapi.yml 93 | ``` 94 | 95 | ```sh 96 | $ ./scripts/test 97 | ``` 98 | 99 | ## Linting and formatting 100 | 101 | This repository uses [ruff](https://github.com/astral-sh/ruff) and 102 | [black](https://github.com/psf/black) to format the code in the repository. 103 | 104 | To lint: 105 | 106 | ```sh 107 | $ ./scripts/lint 108 | ``` 109 | 110 | To format and fix all ruff issues automatically: 111 | 112 | ```sh 113 | $ ./scripts/format 114 | ``` 115 | 116 | ## Publishing and releases 117 | 118 | Changes made to this repository via the automated release PR pipeline should publish to PyPI automatically. If 119 | the changes aren't made through the automated pipeline, you may want to make releases manually. 120 | 121 | ### Publish with a GitHub workflow 122 | 123 | You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/RetellAI/retell-python-sdk/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. 124 | 125 | ### Publish manually 126 | 127 | If you need to manually release a package, you can run the `bin/publish-pypi` script with a `PYPI_TOKEN` set on 128 | the environment. 129 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting Security Issues 4 | 5 | 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. 6 | 7 | To report a security issue, please contact the Stainless team at security@stainless.com. 8 | 9 | ## Responsible Disclosure 10 | 11 | We appreciate the efforts of security researchers and individuals who help us maintain the security of 12 | SDKs we generate. If you believe you have found a security vulnerability, please adhere to responsible 13 | disclosure practices by allowing us a reasonable amount of time to investigate and address the issue 14 | before making any information public. 15 | 16 | ## Reporting Non-SDK Related Security Issues 17 | 18 | If you encounter security issues that are not directly related to SDKs but pertain to the services 19 | or products provided by Retell, please follow the respective company's security reporting guidelines. 20 | 21 | ### Retell Terms and Policies 22 | 23 | Please contact support@retellai.com for any questions or concerns regarding the security of our services. 24 | 25 | --- 26 | 27 | Thank you for helping us keep the SDKs and systems they interact with secure. 28 | -------------------------------------------------------------------------------- /bin/check-release-environment: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | errors=() 4 | 5 | if [ -z "${PYPI_TOKEN}" ]; then 6 | errors+=("The RETELL_PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.") 7 | fi 8 | 9 | lenErrors=${#errors[@]} 10 | 11 | if [[ lenErrors -gt 0 ]]; then 12 | echo -e "Found the following errors in the release environment:\n" 13 | 14 | for error in "${errors[@]}"; do 15 | echo -e "- $error\n" 16 | done 17 | 18 | exit 1 19 | fi 20 | 21 | echo "The environment is ready to push releases!" 22 | -------------------------------------------------------------------------------- /bin/publish-pypi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eux 4 | mkdir -p dist 5 | rye build --clean 6 | rye publish --yes --token=$PYPI_TOKEN 7 | -------------------------------------------------------------------------------- /examples/.keep: -------------------------------------------------------------------------------- 1 | File generated from our OpenAPI spec by Stainless. 2 | 3 | This directory can be used to store example files demonstrating usage of this SDK. 4 | It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | pretty = True 3 | show_error_codes = True 4 | 5 | # Exclude _files.py because mypy isn't smart enough to apply 6 | # the correct type narrowing and as this is an internal module 7 | # it's fine to just use Pyright. 8 | # 9 | # We also exclude our `tests` as mypy doesn't always infer 10 | # types correctly and Pyright will still catch any type errors. 11 | exclude = ^(src/retell/_files\.py|_dev/.*\.py|tests/.*)$ 12 | 13 | strict_equality = True 14 | implicit_reexport = True 15 | check_untyped_defs = True 16 | no_implicit_optional = True 17 | 18 | warn_return_any = True 19 | warn_unreachable = True 20 | warn_unused_configs = True 21 | 22 | # Turn these options off as it could cause conflicts 23 | # with the Pyright options. 24 | warn_unused_ignores = False 25 | warn_redundant_casts = False 26 | 27 | disallow_any_generics = True 28 | disallow_untyped_defs = True 29 | disallow_untyped_calls = True 30 | disallow_subclassing_any = True 31 | disallow_incomplete_defs = True 32 | disallow_untyped_decorators = True 33 | cache_fine_grained = True 34 | 35 | # By default, mypy reports an error if you assign a value to the result 36 | # of a function call that doesn't return anything. We do this in our test 37 | # cases: 38 | # ``` 39 | # result = ... 40 | # assert result is None 41 | # ``` 42 | # Changing this codegen to make mypy happy would increase complexity 43 | # and would not be worth it. 44 | disable_error_code = func-returns-value,overload-cannot-match 45 | 46 | # https://github.com/python/mypy/issues/12162 47 | [mypy.overrides] 48 | module = "black.files.*" 49 | ignore_errors = true 50 | ignore_missing_imports = true 51 | -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | import nox 2 | 3 | 4 | @nox.session(reuse_venv=True, name="test-pydantic-v1") 5 | def test_pydantic_v1(session: nox.Session) -> None: 6 | session.install("-r", "requirements-dev.lock") 7 | session.install("pydantic<2") 8 | 9 | session.run("pytest", "--showlocals", "--ignore=tests/functional", *session.posargs) 10 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "retell-sdk" 3 | version = "4.33.0" 4 | description = "The official Python library for the retell API" 5 | dynamic = ["readme"] 6 | license = "Apache-2.0" 7 | authors = [ 8 | { name = "Retell", email = "support@retellai.com" }, 9 | ] 10 | dependencies = [ 11 | "httpx>=0.23.0, <1", 12 | "pydantic>=1.9.0, <3", 13 | "typing-extensions>=4.10, <5", 14 | "anyio>=3.5.0, <5", 15 | "distro>=1.7.0, <2", 16 | "sniffio", 17 | "cached-property; python_version < '3.8'", 18 | "cryptography", 19 | ] 20 | requires-python = ">= 3.8" 21 | classifiers = [ 22 | "Typing :: Typed", 23 | "Intended Audience :: Developers", 24 | "Programming Language :: Python :: 3.8", 25 | "Programming Language :: Python :: 3.9", 26 | "Programming Language :: Python :: 3.10", 27 | "Programming Language :: Python :: 3.11", 28 | "Programming Language :: Python :: 3.12", 29 | "Operating System :: OS Independent", 30 | "Operating System :: POSIX", 31 | "Operating System :: MacOS", 32 | "Operating System :: POSIX :: Linux", 33 | "Operating System :: Microsoft :: Windows", 34 | "Topic :: Software Development :: Libraries :: Python Modules", 35 | "License :: OSI Approved :: Apache Software License" 36 | ] 37 | 38 | [project.urls] 39 | Homepage = "https://github.com/RetellAI/retell-python-sdk" 40 | Repository = "https://github.com/RetellAI/retell-python-sdk" 41 | 42 | 43 | [tool.rye] 44 | managed = true 45 | # version pins are in requirements-dev.lock 46 | dev-dependencies = [ 47 | "pyright==1.1.399", 48 | "mypy", 49 | "respx", 50 | "pytest", 51 | "pytest-asyncio", 52 | "ruff", 53 | "time-machine", 54 | "nox", 55 | "dirty-equals>=0.6.0", 56 | "importlib-metadata>=6.7.0", 57 | "rich>=13.7.1", 58 | "nest_asyncio==1.6.0", 59 | ] 60 | 61 | [tool.rye.scripts] 62 | format = { chain = [ 63 | "format:ruff", 64 | "format:docs", 65 | "fix:ruff", 66 | # run formatting again to fix any inconsistencies when imports are stripped 67 | "format:ruff", 68 | ]} 69 | "format:docs" = "python scripts/utils/ruffen-docs.py README.md api.md" 70 | "format:ruff" = "ruff format" 71 | 72 | "lint" = { chain = [ 73 | "check:ruff", 74 | "typecheck", 75 | "check:importable", 76 | ]} 77 | "check:ruff" = "ruff check ." 78 | "fix:ruff" = "ruff check --fix ." 79 | 80 | "check:importable" = "python -c 'import retell'" 81 | 82 | typecheck = { chain = [ 83 | "typecheck:pyright", 84 | "typecheck:mypy" 85 | ]} 86 | "typecheck:pyright" = "pyright" 87 | "typecheck:verify-types" = "pyright --verifytypes retell --ignoreexternal" 88 | "typecheck:mypy" = "mypy ." 89 | 90 | [build-system] 91 | requires = ["hatchling==1.26.3", "hatch-fancy-pypi-readme"] 92 | build-backend = "hatchling.build" 93 | 94 | [tool.hatch.build] 95 | include = [ 96 | "src/*" 97 | ] 98 | 99 | [tool.hatch.build.targets.wheel] 100 | packages = ["src/retell"] 101 | 102 | [tool.hatch.build.targets.sdist] 103 | # Basically everything except hidden files/directories (such as .github, .devcontainers, .python-version, etc) 104 | include = [ 105 | "/*.toml", 106 | "/*.json", 107 | "/*.lock", 108 | "/*.md", 109 | "/mypy.ini", 110 | "/noxfile.py", 111 | "bin/*", 112 | "examples/*", 113 | "src/*", 114 | "tests/*", 115 | ] 116 | 117 | [tool.hatch.metadata.hooks.fancy-pypi-readme] 118 | content-type = "text/markdown" 119 | 120 | [[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] 121 | path = "README.md" 122 | 123 | [[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]] 124 | # replace relative links with absolute links 125 | pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)' 126 | replacement = '[\1](https://github.com/RetellAI/retell-python-sdk/tree/main/\g<2>)' 127 | 128 | [tool.pytest.ini_options] 129 | testpaths = ["tests"] 130 | addopts = "--tb=short" 131 | xfail_strict = true 132 | asyncio_mode = "auto" 133 | asyncio_default_fixture_loop_scope = "session" 134 | filterwarnings = [ 135 | "error" 136 | ] 137 | 138 | [tool.pyright] 139 | # this enables practically every flag given by pyright. 140 | # there are a couple of flags that are still disabled by 141 | # default in strict mode as they are experimental and niche. 142 | typeCheckingMode = "strict" 143 | pythonVersion = "3.8" 144 | 145 | exclude = [ 146 | "_dev", 147 | ".venv", 148 | ".nox", 149 | ] 150 | 151 | reportImplicitOverride = true 152 | reportOverlappingOverload = false 153 | 154 | reportImportCycles = false 155 | reportPrivateUsage = false 156 | 157 | [tool.ruff] 158 | line-length = 120 159 | output-format = "grouped" 160 | target-version = "py37" 161 | 162 | [tool.ruff.format] 163 | docstring-code-format = true 164 | 165 | [tool.ruff.lint] 166 | select = [ 167 | # isort 168 | "I", 169 | # bugbear rules 170 | "B", 171 | # remove unused imports 172 | "F401", 173 | # bare except statements 174 | "E722", 175 | # unused arguments 176 | "ARG", 177 | # print statements 178 | "T201", 179 | "T203", 180 | # misuse of typing.TYPE_CHECKING 181 | "TC004", 182 | # import rules 183 | "TID251", 184 | ] 185 | ignore = [ 186 | # mutable defaults 187 | "B006", 188 | ] 189 | unfixable = [ 190 | # disable auto fix for print statements 191 | "T201", 192 | "T203", 193 | ] 194 | 195 | [tool.ruff.lint.flake8-tidy-imports.banned-api] 196 | "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" 197 | 198 | [tool.ruff.lint.isort] 199 | length-sort = true 200 | length-sort-straight = true 201 | combine-as-imports = true 202 | extra-standard-library = ["typing_extensions"] 203 | known-first-party = ["retell", "tests"] 204 | 205 | [tool.ruff.lint.per-file-ignores] 206 | "bin/**.py" = ["T201", "T203"] 207 | "scripts/**.py" = ["T201", "T203"] 208 | "tests/**.py" = ["T201", "T203"] 209 | "examples/**.py" = ["T201", "T203"] 210 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": { 3 | ".": {} 4 | }, 5 | "$schema": "https://raw.githubusercontent.com/stainless-api/release-please/main/schemas/config.json", 6 | "include-v-in-tag": true, 7 | "include-component-in-tag": false, 8 | "versioning": "prerelease", 9 | "prerelease": true, 10 | "bump-minor-pre-major": true, 11 | "bump-patch-for-minor-pre-major": false, 12 | "pull-request-header": "Automated Release PR", 13 | "pull-request-title-pattern": "release: ${version}", 14 | "changelog-sections": [ 15 | { 16 | "type": "feat", 17 | "section": "Features" 18 | }, 19 | { 20 | "type": "fix", 21 | "section": "Bug Fixes" 22 | }, 23 | { 24 | "type": "perf", 25 | "section": "Performance Improvements" 26 | }, 27 | { 28 | "type": "revert", 29 | "section": "Reverts" 30 | }, 31 | { 32 | "type": "chore", 33 | "section": "Chores" 34 | }, 35 | { 36 | "type": "docs", 37 | "section": "Documentation" 38 | }, 39 | { 40 | "type": "style", 41 | "section": "Styles" 42 | }, 43 | { 44 | "type": "refactor", 45 | "section": "Refactors" 46 | }, 47 | { 48 | "type": "test", 49 | "section": "Tests", 50 | "hidden": true 51 | }, 52 | { 53 | "type": "build", 54 | "section": "Build System" 55 | }, 56 | { 57 | "type": "ci", 58 | "section": "Continuous Integration", 59 | "hidden": true 60 | } 61 | ], 62 | "release-type": "python", 63 | "extra-files": [ 64 | "src/retell/_version.py" 65 | ] 66 | } -------------------------------------------------------------------------------- /requirements-dev.lock: -------------------------------------------------------------------------------- 1 | # generated by rye 2 | # use `rye lock` or `rye sync` to update this lockfile 3 | # 4 | # last locked with the following flags: 5 | # pre: false 6 | # features: [] 7 | # all-features: true 8 | # with-sources: false 9 | # generate-hashes: false 10 | # universal: false 11 | 12 | -e file:. 13 | annotated-types==0.6.0 14 | # via pydantic 15 | anyio==4.4.0 16 | # via httpx 17 | # via retell-sdk 18 | argcomplete==3.1.2 19 | # via nox 20 | certifi==2023.7.22 21 | # via httpcore 22 | # via httpx 23 | cffi==1.17.1 24 | # via cryptography 25 | colorlog==6.7.0 26 | # via nox 27 | cryptography==44.0.0 28 | # via retell-sdk 29 | dirty-equals==0.6.0 30 | distlib==0.3.7 31 | # via virtualenv 32 | distro==1.8.0 33 | # via retell-sdk 34 | exceptiongroup==1.2.2 35 | # via anyio 36 | # via pytest 37 | filelock==3.12.4 38 | # via virtualenv 39 | h11==0.14.0 40 | # via httpcore 41 | httpcore==1.0.2 42 | # via httpx 43 | httpx==0.28.1 44 | # via respx 45 | # via retell-sdk 46 | idna==3.4 47 | # via anyio 48 | # via httpx 49 | importlib-metadata==7.0.0 50 | iniconfig==2.0.0 51 | # via pytest 52 | markdown-it-py==3.0.0 53 | # via rich 54 | mdurl==0.1.2 55 | # via markdown-it-py 56 | mypy==1.14.1 57 | mypy-extensions==1.0.0 58 | # via mypy 59 | nest-asyncio==1.6.0 60 | nodeenv==1.8.0 61 | # via pyright 62 | nox==2023.4.22 63 | packaging==23.2 64 | # via nox 65 | # via pytest 66 | platformdirs==3.11.0 67 | # via virtualenv 68 | pluggy==1.5.0 69 | # via pytest 70 | pycparser==2.22 71 | # via cffi 72 | pydantic==2.10.3 73 | # via retell-sdk 74 | pydantic-core==2.27.1 75 | # via pydantic 76 | pygments==2.18.0 77 | # via rich 78 | pyright==1.1.399 79 | pytest==8.3.3 80 | # via pytest-asyncio 81 | pytest-asyncio==0.24.0 82 | python-dateutil==2.8.2 83 | # via time-machine 84 | pytz==2023.3.post1 85 | # via dirty-equals 86 | respx==0.22.0 87 | rich==13.7.1 88 | ruff==0.9.4 89 | setuptools==68.2.2 90 | # via nodeenv 91 | six==1.16.0 92 | # via python-dateutil 93 | sniffio==1.3.0 94 | # via anyio 95 | # via retell-sdk 96 | time-machine==2.9.0 97 | tomli==2.0.2 98 | # via mypy 99 | # via pytest 100 | typing-extensions==4.12.2 101 | # via anyio 102 | # via mypy 103 | # via pydantic 104 | # via pydantic-core 105 | # via pyright 106 | # via retell-sdk 107 | virtualenv==20.24.5 108 | # via nox 109 | zipp==3.17.0 110 | # via importlib-metadata 111 | -------------------------------------------------------------------------------- /requirements.lock: -------------------------------------------------------------------------------- 1 | # generated by rye 2 | # use `rye lock` or `rye sync` to update this lockfile 3 | # 4 | # last locked with the following flags: 5 | # pre: false 6 | # features: [] 7 | # all-features: true 8 | # with-sources: false 9 | # generate-hashes: false 10 | # universal: false 11 | 12 | -e file:. 13 | annotated-types==0.6.0 14 | # via pydantic 15 | anyio==4.4.0 16 | # via httpx 17 | # via retell-sdk 18 | certifi==2023.7.22 19 | # via httpcore 20 | # via httpx 21 | cffi==1.17.1 22 | # via cryptography 23 | cryptography==44.0.0 24 | # via retell-sdk 25 | distro==1.8.0 26 | # via retell-sdk 27 | exceptiongroup==1.2.2 28 | # via anyio 29 | h11==0.14.0 30 | # via httpcore 31 | httpcore==1.0.2 32 | # via httpx 33 | httpx==0.28.1 34 | # via retell-sdk 35 | idna==3.4 36 | # via anyio 37 | # via httpx 38 | pycparser==2.22 39 | # via cffi 40 | pydantic==2.10.3 41 | # via retell-sdk 42 | pydantic-core==2.27.1 43 | # via pydantic 44 | sniffio==1.3.0 45 | # via anyio 46 | # via retell-sdk 47 | typing-extensions==4.12.2 48 | # via anyio 49 | # via pydantic 50 | # via pydantic-core 51 | # via retell-sdk 52 | -------------------------------------------------------------------------------- /scripts/bootstrap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | if ! command -v rye >/dev/null 2>&1 && [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ]; then 8 | brew bundle check >/dev/null 2>&1 || { 9 | echo "==> Installing Homebrew dependencies…" 10 | brew bundle 11 | } 12 | fi 13 | 14 | echo "==> Installing Python dependencies…" 15 | 16 | # experimental uv support makes installations significantly faster 17 | rye config --set-bool behavior.use-uv=true 18 | 19 | rye sync --all-features 20 | -------------------------------------------------------------------------------- /scripts/format: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | echo "==> Running formatters" 8 | rye run format 9 | -------------------------------------------------------------------------------- /scripts/lint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | echo "==> Running lints" 8 | rye run lint 9 | 10 | echo "==> Making sure it imports" 11 | rye run python -c 'import retell' 12 | -------------------------------------------------------------------------------- /scripts/mock: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | if [[ -n "$1" && "$1" != '--'* ]]; then 8 | URL="$1" 9 | shift 10 | else 11 | URL="$(grep 'openapi_spec_url' .stats.yml | cut -d' ' -f2)" 12 | fi 13 | 14 | # Check if the URL is empty 15 | if [ -z "$URL" ]; then 16 | echo "Error: No OpenAPI spec path/url provided or found in .stats.yml" 17 | exit 1 18 | fi 19 | 20 | echo "==> Starting mock server with URL ${URL}" 21 | 22 | # Run prism mock on the given spec 23 | if [ "$1" == "--daemon" ]; then 24 | npm exec --package=@stainless-api/prism-cli@5.8.5 -- prism mock "$URL" &> .prism.log & 25 | 26 | # Wait for server to come online 27 | echo -n "Waiting for server" 28 | while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do 29 | echo -n "." 30 | sleep 0.1 31 | done 32 | 33 | if grep -q "✖ fatal" ".prism.log"; then 34 | cat .prism.log 35 | exit 1 36 | fi 37 | 38 | echo 39 | else 40 | npm exec --package=@stainless-api/prism-cli@5.8.5 -- prism mock "$URL" 41 | fi 42 | -------------------------------------------------------------------------------- /scripts/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | RED='\033[0;31m' 8 | GREEN='\033[0;32m' 9 | YELLOW='\033[0;33m' 10 | NC='\033[0m' # No Color 11 | 12 | function prism_is_running() { 13 | curl --silent "http://localhost:4010" >/dev/null 2>&1 14 | } 15 | 16 | kill_server_on_port() { 17 | pids=$(lsof -t -i tcp:"$1" || echo "") 18 | if [ "$pids" != "" ]; then 19 | kill "$pids" 20 | echo "Stopped $pids." 21 | fi 22 | } 23 | 24 | function is_overriding_api_base_url() { 25 | [ -n "$TEST_API_BASE_URL" ] 26 | } 27 | 28 | if ! is_overriding_api_base_url && ! prism_is_running ; then 29 | # When we exit this script, make sure to kill the background mock server process 30 | trap 'kill_server_on_port 4010' EXIT 31 | 32 | # Start the dev server 33 | ./scripts/mock --daemon 34 | fi 35 | 36 | if is_overriding_api_base_url ; then 37 | echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}" 38 | echo 39 | elif ! prism_is_running ; then 40 | echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server" 41 | echo -e "running against your OpenAPI spec." 42 | echo 43 | echo -e "To run the server, pass in the path or url of your OpenAPI" 44 | echo -e "spec to the prism command:" 45 | echo 46 | echo -e " \$ ${YELLOW}npm exec --package=@stoplight/prism-cli@~5.3.2 -- prism mock path/to/your.openapi.yml${NC}" 47 | echo 48 | 49 | exit 1 50 | else 51 | echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}" 52 | echo 53 | fi 54 | 55 | export DEFER_PYDANTIC_BUILD=false 56 | 57 | echo "==> Running tests" 58 | rye run pytest "$@" 59 | -------------------------------------------------------------------------------- /scripts/utils/ruffen-docs.py: -------------------------------------------------------------------------------- 1 | # fork of https://github.com/asottile/blacken-docs adapted for ruff 2 | from __future__ import annotations 3 | 4 | import re 5 | import sys 6 | import argparse 7 | import textwrap 8 | import contextlib 9 | import subprocess 10 | from typing import Match, Optional, Sequence, Generator, NamedTuple, cast 11 | 12 | MD_RE = re.compile( 13 | r"(?P^(?P *)```\s*python\n)" r"(?P.*?)" r"(?P^(?P=indent)```\s*$)", 14 | re.DOTALL | re.MULTILINE, 15 | ) 16 | MD_PYCON_RE = re.compile( 17 | r"(?P^(?P *)```\s*pycon\n)" r"(?P.*?)" r"(?P^(?P=indent)```.*$)", 18 | re.DOTALL | re.MULTILINE, 19 | ) 20 | PYCON_PREFIX = ">>> " 21 | PYCON_CONTINUATION_PREFIX = "..." 22 | PYCON_CONTINUATION_RE = re.compile( 23 | rf"^{re.escape(PYCON_CONTINUATION_PREFIX)}( |$)", 24 | ) 25 | DEFAULT_LINE_LENGTH = 100 26 | 27 | 28 | class CodeBlockError(NamedTuple): 29 | offset: int 30 | exc: Exception 31 | 32 | 33 | def format_str( 34 | src: str, 35 | ) -> tuple[str, Sequence[CodeBlockError]]: 36 | errors: list[CodeBlockError] = [] 37 | 38 | @contextlib.contextmanager 39 | def _collect_error(match: Match[str]) -> Generator[None, None, None]: 40 | try: 41 | yield 42 | except Exception as e: 43 | errors.append(CodeBlockError(match.start(), e)) 44 | 45 | def _md_match(match: Match[str]) -> str: 46 | code = textwrap.dedent(match["code"]) 47 | with _collect_error(match): 48 | code = format_code_block(code) 49 | code = textwrap.indent(code, match["indent"]) 50 | return f"{match['before']}{code}{match['after']}" 51 | 52 | def _pycon_match(match: Match[str]) -> str: 53 | code = "" 54 | fragment = cast(Optional[str], None) 55 | 56 | def finish_fragment() -> None: 57 | nonlocal code 58 | nonlocal fragment 59 | 60 | if fragment is not None: 61 | with _collect_error(match): 62 | fragment = format_code_block(fragment) 63 | fragment_lines = fragment.splitlines() 64 | code += f"{PYCON_PREFIX}{fragment_lines[0]}\n" 65 | for line in fragment_lines[1:]: 66 | # Skip blank lines to handle Black adding a blank above 67 | # functions within blocks. A blank line would end the REPL 68 | # continuation prompt. 69 | # 70 | # >>> if True: 71 | # ... def f(): 72 | # ... pass 73 | # ... 74 | if line: 75 | code += f"{PYCON_CONTINUATION_PREFIX} {line}\n" 76 | if fragment_lines[-1].startswith(" "): 77 | code += f"{PYCON_CONTINUATION_PREFIX}\n" 78 | fragment = None 79 | 80 | indentation = None 81 | for line in match["code"].splitlines(): 82 | orig_line, line = line, line.lstrip() 83 | if indentation is None and line: 84 | indentation = len(orig_line) - len(line) 85 | continuation_match = PYCON_CONTINUATION_RE.match(line) 86 | if continuation_match and fragment is not None: 87 | fragment += line[continuation_match.end() :] + "\n" 88 | else: 89 | finish_fragment() 90 | if line.startswith(PYCON_PREFIX): 91 | fragment = line[len(PYCON_PREFIX) :] + "\n" 92 | else: 93 | code += orig_line[indentation:] + "\n" 94 | finish_fragment() 95 | return code 96 | 97 | def _md_pycon_match(match: Match[str]) -> str: 98 | code = _pycon_match(match) 99 | code = textwrap.indent(code, match["indent"]) 100 | return f"{match['before']}{code}{match['after']}" 101 | 102 | src = MD_RE.sub(_md_match, src) 103 | src = MD_PYCON_RE.sub(_md_pycon_match, src) 104 | return src, errors 105 | 106 | 107 | def format_code_block(code: str) -> str: 108 | return subprocess.check_output( 109 | [ 110 | sys.executable, 111 | "-m", 112 | "ruff", 113 | "format", 114 | "--stdin-filename=script.py", 115 | f"--line-length={DEFAULT_LINE_LENGTH}", 116 | ], 117 | encoding="utf-8", 118 | input=code, 119 | ) 120 | 121 | 122 | def format_file( 123 | filename: str, 124 | skip_errors: bool, 125 | ) -> int: 126 | with open(filename, encoding="UTF-8") as f: 127 | contents = f.read() 128 | new_contents, errors = format_str(contents) 129 | for error in errors: 130 | lineno = contents[: error.offset].count("\n") + 1 131 | print(f"{filename}:{lineno}: code block parse error {error.exc}") 132 | if errors and not skip_errors: 133 | return 1 134 | if contents != new_contents: 135 | print(f"{filename}: Rewriting...") 136 | with open(filename, "w", encoding="UTF-8") as f: 137 | f.write(new_contents) 138 | return 0 139 | else: 140 | return 0 141 | 142 | 143 | def main(argv: Sequence[str] | None = None) -> int: 144 | parser = argparse.ArgumentParser() 145 | parser.add_argument( 146 | "-l", 147 | "--line-length", 148 | type=int, 149 | default=DEFAULT_LINE_LENGTH, 150 | ) 151 | parser.add_argument( 152 | "-S", 153 | "--skip-string-normalization", 154 | action="store_true", 155 | ) 156 | parser.add_argument("-E", "--skip-errors", action="store_true") 157 | parser.add_argument("filenames", nargs="*") 158 | args = parser.parse_args(argv) 159 | 160 | retv = 0 161 | for filename in args.filenames: 162 | retv |= format_file(filename, skip_errors=args.skip_errors) 163 | return retv 164 | 165 | 166 | if __name__ == "__main__": 167 | raise SystemExit(main()) 168 | -------------------------------------------------------------------------------- /scripts/utils/upload-artifact.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -exuo pipefail 3 | 4 | RESPONSE=$(curl -X POST "$URL" \ 5 | -H "Authorization: Bearer $AUTH" \ 6 | -H "Content-Type: application/json") 7 | 8 | SIGNED_URL=$(echo "$RESPONSE" | jq -r '.url') 9 | 10 | if [[ "$SIGNED_URL" == "null" ]]; then 11 | echo -e "\033[31mFailed to get signed URL.\033[0m" 12 | exit 1 13 | fi 14 | 15 | UPLOAD_RESPONSE=$(tar -cz . | curl -v -X PUT \ 16 | -H "Content-Type: application/gzip" \ 17 | --data-binary @- "$SIGNED_URL" 2>&1) 18 | 19 | if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then 20 | echo -e "\033[32mUploaded build to Stainless storage.\033[0m" 21 | echo -e "\033[32mInstallation: pip install 'https://pkg.stainless.com/s/toddlzt-python/$SHA'\033[0m" 22 | else 23 | echo -e "\033[31mFailed to upload artifact.\033[0m" 24 | exit 1 25 | fi 26 | -------------------------------------------------------------------------------- /src/retell/__init__.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | import typing as _t 4 | 5 | from . import types 6 | from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes 7 | from ._utils import file_from_path 8 | from ._client import Client, Retell, Stream, Timeout, Transport, AsyncClient, AsyncRetell, AsyncStream, RequestOptions 9 | from ._models import BaseModel 10 | from ._version import __title__, __version__ 11 | from ._response import APIResponse as APIResponse, AsyncAPIResponse as AsyncAPIResponse 12 | from ._constants import DEFAULT_TIMEOUT, DEFAULT_MAX_RETRIES, DEFAULT_CONNECTION_LIMITS 13 | from ._exceptions import ( 14 | APIError, 15 | RetellError, 16 | ConflictError, 17 | NotFoundError, 18 | APIStatusError, 19 | RateLimitError, 20 | APITimeoutError, 21 | BadRequestError, 22 | APIConnectionError, 23 | AuthenticationError, 24 | InternalServerError, 25 | PermissionDeniedError, 26 | UnprocessableEntityError, 27 | APIResponseValidationError, 28 | ) 29 | from ._base_client import DefaultHttpxClient, DefaultAsyncHttpxClient 30 | from ._utils._logs import setup_logging as _setup_logging 31 | 32 | __all__ = [ 33 | "types", 34 | "__version__", 35 | "__title__", 36 | "NoneType", 37 | "Transport", 38 | "ProxiesTypes", 39 | "NotGiven", 40 | "NOT_GIVEN", 41 | "Omit", 42 | "RetellError", 43 | "APIError", 44 | "APIStatusError", 45 | "APITimeoutError", 46 | "APIConnectionError", 47 | "APIResponseValidationError", 48 | "BadRequestError", 49 | "AuthenticationError", 50 | "PermissionDeniedError", 51 | "NotFoundError", 52 | "ConflictError", 53 | "UnprocessableEntityError", 54 | "RateLimitError", 55 | "InternalServerError", 56 | "Timeout", 57 | "RequestOptions", 58 | "Client", 59 | "AsyncClient", 60 | "Stream", 61 | "AsyncStream", 62 | "Retell", 63 | "AsyncRetell", 64 | "file_from_path", 65 | "BaseModel", 66 | "DEFAULT_TIMEOUT", 67 | "DEFAULT_MAX_RETRIES", 68 | "DEFAULT_CONNECTION_LIMITS", 69 | "DefaultHttpxClient", 70 | "DefaultAsyncHttpxClient", 71 | ] 72 | 73 | if not _t.TYPE_CHECKING: 74 | from ._utils._resources_proxy import resources as resources 75 | 76 | _setup_logging() 77 | 78 | # Update the __module__ attribute for exported symbols so that 79 | # error messages point to this module instead of the module 80 | # it was originally defined in, e.g. 81 | # retell._exceptions.NotFoundError -> retell.NotFoundError 82 | __locals = locals() 83 | for __name in __all__: 84 | if not __name.startswith("__"): 85 | try: 86 | __locals[__name].__module__ = "retell" 87 | except (TypeError, AttributeError): 88 | # Some of our exported symbols are builtins which we can't set attributes for. 89 | pass 90 | -------------------------------------------------------------------------------- /src/retell/_compat.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload 4 | from datetime import date, datetime 5 | from typing_extensions import Self, Literal 6 | 7 | import pydantic 8 | from pydantic.fields import FieldInfo 9 | 10 | from ._types import IncEx, StrBytesIntFloat 11 | 12 | _T = TypeVar("_T") 13 | _ModelT = TypeVar("_ModelT", bound=pydantic.BaseModel) 14 | 15 | # --------------- Pydantic v2 compatibility --------------- 16 | 17 | # Pyright incorrectly reports some of our functions as overriding a method when they don't 18 | # pyright: reportIncompatibleMethodOverride=false 19 | 20 | PYDANTIC_V2 = pydantic.VERSION.startswith("2.") 21 | 22 | # v1 re-exports 23 | if TYPE_CHECKING: 24 | 25 | def parse_date(value: date | StrBytesIntFloat) -> date: # noqa: ARG001 26 | ... 27 | 28 | def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime: # noqa: ARG001 29 | ... 30 | 31 | def get_args(t: type[Any]) -> tuple[Any, ...]: # noqa: ARG001 32 | ... 33 | 34 | def is_union(tp: type[Any] | None) -> bool: # noqa: ARG001 35 | ... 36 | 37 | def get_origin(t: type[Any]) -> type[Any] | None: # noqa: ARG001 38 | ... 39 | 40 | def is_literal_type(type_: type[Any]) -> bool: # noqa: ARG001 41 | ... 42 | 43 | def is_typeddict(type_: type[Any]) -> bool: # noqa: ARG001 44 | ... 45 | 46 | else: 47 | if PYDANTIC_V2: 48 | from pydantic.v1.typing import ( 49 | get_args as get_args, 50 | is_union as is_union, 51 | get_origin as get_origin, 52 | is_typeddict as is_typeddict, 53 | is_literal_type as is_literal_type, 54 | ) 55 | from pydantic.v1.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime 56 | else: 57 | from pydantic.typing import ( 58 | get_args as get_args, 59 | is_union as is_union, 60 | get_origin as get_origin, 61 | is_typeddict as is_typeddict, 62 | is_literal_type as is_literal_type, 63 | ) 64 | from pydantic.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime 65 | 66 | 67 | # refactored config 68 | if TYPE_CHECKING: 69 | from pydantic import ConfigDict as ConfigDict 70 | else: 71 | if PYDANTIC_V2: 72 | from pydantic import ConfigDict 73 | else: 74 | # TODO: provide an error message here? 75 | ConfigDict = None 76 | 77 | 78 | # renamed methods / properties 79 | def parse_obj(model: type[_ModelT], value: object) -> _ModelT: 80 | if PYDANTIC_V2: 81 | return model.model_validate(value) 82 | else: 83 | return cast(_ModelT, model.parse_obj(value)) # pyright: ignore[reportDeprecated, reportUnnecessaryCast] 84 | 85 | 86 | def field_is_required(field: FieldInfo) -> bool: 87 | if PYDANTIC_V2: 88 | return field.is_required() 89 | return field.required # type: ignore 90 | 91 | 92 | def field_get_default(field: FieldInfo) -> Any: 93 | value = field.get_default() 94 | if PYDANTIC_V2: 95 | from pydantic_core import PydanticUndefined 96 | 97 | if value == PydanticUndefined: 98 | return None 99 | return value 100 | return value 101 | 102 | 103 | def field_outer_type(field: FieldInfo) -> Any: 104 | if PYDANTIC_V2: 105 | return field.annotation 106 | return field.outer_type_ # type: ignore 107 | 108 | 109 | def get_model_config(model: type[pydantic.BaseModel]) -> Any: 110 | if PYDANTIC_V2: 111 | return model.model_config 112 | return model.__config__ # type: ignore 113 | 114 | 115 | def get_model_fields(model: type[pydantic.BaseModel]) -> dict[str, FieldInfo]: 116 | if PYDANTIC_V2: 117 | return model.model_fields 118 | return model.__fields__ # type: ignore 119 | 120 | 121 | def model_copy(model: _ModelT, *, deep: bool = False) -> _ModelT: 122 | if PYDANTIC_V2: 123 | return model.model_copy(deep=deep) 124 | return model.copy(deep=deep) # type: ignore 125 | 126 | 127 | def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str: 128 | if PYDANTIC_V2: 129 | return model.model_dump_json(indent=indent) 130 | return model.json(indent=indent) # type: ignore 131 | 132 | 133 | def model_dump( 134 | model: pydantic.BaseModel, 135 | *, 136 | exclude: IncEx | None = None, 137 | exclude_unset: bool = False, 138 | exclude_defaults: bool = False, 139 | warnings: bool = True, 140 | mode: Literal["json", "python"] = "python", 141 | ) -> dict[str, Any]: 142 | if PYDANTIC_V2 or hasattr(model, "model_dump"): 143 | return model.model_dump( 144 | mode=mode, 145 | exclude=exclude, 146 | exclude_unset=exclude_unset, 147 | exclude_defaults=exclude_defaults, 148 | # warnings are not supported in Pydantic v1 149 | warnings=warnings if PYDANTIC_V2 else True, 150 | ) 151 | return cast( 152 | "dict[str, Any]", 153 | model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast] 154 | exclude=exclude, 155 | exclude_unset=exclude_unset, 156 | exclude_defaults=exclude_defaults, 157 | ), 158 | ) 159 | 160 | 161 | def model_parse(model: type[_ModelT], data: Any) -> _ModelT: 162 | if PYDANTIC_V2: 163 | return model.model_validate(data) 164 | return model.parse_obj(data) # pyright: ignore[reportDeprecated] 165 | 166 | 167 | # generic models 168 | if TYPE_CHECKING: 169 | 170 | class GenericModel(pydantic.BaseModel): ... 171 | 172 | else: 173 | if PYDANTIC_V2: 174 | # there no longer needs to be a distinction in v2 but 175 | # we still have to create our own subclass to avoid 176 | # inconsistent MRO ordering errors 177 | class GenericModel(pydantic.BaseModel): ... 178 | 179 | else: 180 | import pydantic.generics 181 | 182 | class GenericModel(pydantic.generics.GenericModel, pydantic.BaseModel): ... 183 | 184 | 185 | # cached properties 186 | if TYPE_CHECKING: 187 | cached_property = property 188 | 189 | # we define a separate type (copied from typeshed) 190 | # that represents that `cached_property` is `set`able 191 | # at runtime, which differs from `@property`. 192 | # 193 | # this is a separate type as editors likely special case 194 | # `@property` and we don't want to cause issues just to have 195 | # more helpful internal types. 196 | 197 | class typed_cached_property(Generic[_T]): 198 | func: Callable[[Any], _T] 199 | attrname: str | None 200 | 201 | def __init__(self, func: Callable[[Any], _T]) -> None: ... 202 | 203 | @overload 204 | def __get__(self, instance: None, owner: type[Any] | None = None) -> Self: ... 205 | 206 | @overload 207 | def __get__(self, instance: object, owner: type[Any] | None = None) -> _T: ... 208 | 209 | def __get__(self, instance: object, owner: type[Any] | None = None) -> _T | Self: 210 | raise NotImplementedError() 211 | 212 | def __set_name__(self, owner: type[Any], name: str) -> None: ... 213 | 214 | # __set__ is not defined at runtime, but @cached_property is designed to be settable 215 | def __set__(self, instance: object, value: _T) -> None: ... 216 | else: 217 | from functools import cached_property as cached_property 218 | 219 | typed_cached_property = cached_property 220 | -------------------------------------------------------------------------------- /src/retell/_constants.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | import httpx 4 | 5 | RAW_RESPONSE_HEADER = "X-Stainless-Raw-Response" 6 | OVERRIDE_CAST_TO_HEADER = "____stainless_override_cast_to" 7 | 8 | # default timeout is 1 minute 9 | DEFAULT_TIMEOUT = httpx.Timeout(timeout=60, connect=5.0) 10 | DEFAULT_MAX_RETRIES = 2 11 | DEFAULT_CONNECTION_LIMITS = httpx.Limits(max_connections=100, max_keepalive_connections=20) 12 | 13 | INITIAL_RETRY_DELAY = 0.5 14 | MAX_RETRY_DELAY = 8.0 15 | -------------------------------------------------------------------------------- /src/retell/_exceptions.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from typing_extensions import Literal 6 | 7 | import httpx 8 | 9 | __all__ = [ 10 | "BadRequestError", 11 | "AuthenticationError", 12 | "PermissionDeniedError", 13 | "NotFoundError", 14 | "ConflictError", 15 | "UnprocessableEntityError", 16 | "RateLimitError", 17 | "InternalServerError", 18 | ] 19 | 20 | 21 | class RetellError(Exception): 22 | pass 23 | 24 | 25 | class APIError(RetellError): 26 | message: str 27 | request: httpx.Request 28 | 29 | body: object | None 30 | """The API response body. 31 | 32 | If the API responded with a valid JSON structure then this property will be the 33 | decoded result. 34 | 35 | If it isn't a valid JSON structure then this will be the raw response. 36 | 37 | If there was no response associated with this error then it will be `None`. 38 | """ 39 | 40 | def __init__(self, message: str, request: httpx.Request, *, body: object | None) -> None: # noqa: ARG002 41 | super().__init__(message) 42 | self.request = request 43 | self.message = message 44 | self.body = body 45 | 46 | 47 | class APIResponseValidationError(APIError): 48 | response: httpx.Response 49 | status_code: int 50 | 51 | def __init__(self, response: httpx.Response, body: object | None, *, message: str | None = None) -> None: 52 | super().__init__(message or "Data returned by API invalid for expected schema.", response.request, body=body) 53 | self.response = response 54 | self.status_code = response.status_code 55 | 56 | 57 | class APIStatusError(APIError): 58 | """Raised when an API response has a status code of 4xx or 5xx.""" 59 | 60 | response: httpx.Response 61 | status_code: int 62 | 63 | def __init__(self, message: str, *, response: httpx.Response, body: object | None) -> None: 64 | super().__init__(message, response.request, body=body) 65 | self.response = response 66 | self.status_code = response.status_code 67 | 68 | 69 | class APIConnectionError(APIError): 70 | def __init__(self, *, message: str = "Connection error.", request: httpx.Request) -> None: 71 | super().__init__(message, request, body=None) 72 | 73 | 74 | class APITimeoutError(APIConnectionError): 75 | def __init__(self, request: httpx.Request) -> None: 76 | super().__init__(message="Request timed out.", request=request) 77 | 78 | 79 | class BadRequestError(APIStatusError): 80 | status_code: Literal[400] = 400 # pyright: ignore[reportIncompatibleVariableOverride] 81 | 82 | 83 | class AuthenticationError(APIStatusError): 84 | status_code: Literal[401] = 401 # pyright: ignore[reportIncompatibleVariableOverride] 85 | 86 | 87 | class PermissionDeniedError(APIStatusError): 88 | status_code: Literal[403] = 403 # pyright: ignore[reportIncompatibleVariableOverride] 89 | 90 | 91 | class NotFoundError(APIStatusError): 92 | status_code: Literal[404] = 404 # pyright: ignore[reportIncompatibleVariableOverride] 93 | 94 | 95 | class ConflictError(APIStatusError): 96 | status_code: Literal[409] = 409 # pyright: ignore[reportIncompatibleVariableOverride] 97 | 98 | 99 | class UnprocessableEntityError(APIStatusError): 100 | status_code: Literal[422] = 422 # pyright: ignore[reportIncompatibleVariableOverride] 101 | 102 | 103 | class RateLimitError(APIStatusError): 104 | status_code: Literal[429] = 429 # pyright: ignore[reportIncompatibleVariableOverride] 105 | 106 | 107 | class InternalServerError(APIStatusError): 108 | pass 109 | -------------------------------------------------------------------------------- /src/retell/_files.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import io 4 | import os 5 | import pathlib 6 | from typing import overload 7 | from typing_extensions import TypeGuard 8 | 9 | import anyio 10 | 11 | from ._types import ( 12 | FileTypes, 13 | FileContent, 14 | RequestFiles, 15 | HttpxFileTypes, 16 | Base64FileInput, 17 | HttpxFileContent, 18 | HttpxRequestFiles, 19 | ) 20 | from ._utils import is_tuple_t, is_mapping_t, is_sequence_t 21 | 22 | 23 | def is_base64_file_input(obj: object) -> TypeGuard[Base64FileInput]: 24 | return isinstance(obj, io.IOBase) or isinstance(obj, os.PathLike) 25 | 26 | 27 | def is_file_content(obj: object) -> TypeGuard[FileContent]: 28 | return ( 29 | isinstance(obj, bytes) or isinstance(obj, tuple) or isinstance(obj, io.IOBase) or isinstance(obj, os.PathLike) 30 | ) 31 | 32 | 33 | def assert_is_file_content(obj: object, *, key: str | None = None) -> None: 34 | if not is_file_content(obj): 35 | prefix = f"Expected entry at `{key}`" if key is not None else f"Expected file input `{obj!r}`" 36 | raise RuntimeError( 37 | f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead." 38 | ) from None 39 | 40 | 41 | @overload 42 | def to_httpx_files(files: None) -> None: ... 43 | 44 | 45 | @overload 46 | def to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: ... 47 | 48 | 49 | def to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles | None: 50 | if files is None: 51 | return None 52 | 53 | if is_mapping_t(files): 54 | files = {key: _transform_file(file) for key, file in files.items()} 55 | elif is_sequence_t(files): 56 | files = [(key, _transform_file(file)) for key, file in files] 57 | else: 58 | raise TypeError(f"Unexpected file type input {type(files)}, expected mapping or sequence") 59 | 60 | return files 61 | 62 | 63 | def _transform_file(file: FileTypes) -> HttpxFileTypes: 64 | if is_file_content(file): 65 | if isinstance(file, os.PathLike): 66 | path = pathlib.Path(file) 67 | return (path.name, path.read_bytes()) 68 | 69 | return file 70 | 71 | if is_tuple_t(file): 72 | return (file[0], _read_file_content(file[1]), *file[2:]) 73 | 74 | raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple") 75 | 76 | 77 | def _read_file_content(file: FileContent) -> HttpxFileContent: 78 | if isinstance(file, os.PathLike): 79 | return pathlib.Path(file).read_bytes() 80 | return file 81 | 82 | 83 | @overload 84 | async def async_to_httpx_files(files: None) -> None: ... 85 | 86 | 87 | @overload 88 | async def async_to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: ... 89 | 90 | 91 | async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles | None: 92 | if files is None: 93 | return None 94 | 95 | if is_mapping_t(files): 96 | files = {key: await _async_transform_file(file) for key, file in files.items()} 97 | elif is_sequence_t(files): 98 | files = [(key, await _async_transform_file(file)) for key, file in files] 99 | else: 100 | raise TypeError("Unexpected file type input {type(files)}, expected mapping or sequence") 101 | 102 | return files 103 | 104 | 105 | async def _async_transform_file(file: FileTypes) -> HttpxFileTypes: 106 | if is_file_content(file): 107 | if isinstance(file, os.PathLike): 108 | path = anyio.Path(file) 109 | return (path.name, await path.read_bytes()) 110 | 111 | return file 112 | 113 | if is_tuple_t(file): 114 | return (file[0], await _async_read_file_content(file[1]), *file[2:]) 115 | 116 | raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple") 117 | 118 | 119 | async def _async_read_file_content(file: FileContent) -> HttpxFileContent: 120 | if isinstance(file, os.PathLike): 121 | return await anyio.Path(file).read_bytes() 122 | 123 | return file 124 | -------------------------------------------------------------------------------- /src/retell/_qs.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any, List, Tuple, Union, Mapping, TypeVar 4 | from urllib.parse import parse_qs, urlencode 5 | from typing_extensions import Literal, get_args 6 | 7 | from ._types import NOT_GIVEN, NotGiven, NotGivenOr 8 | from ._utils import flatten 9 | 10 | _T = TypeVar("_T") 11 | 12 | 13 | ArrayFormat = Literal["comma", "repeat", "indices", "brackets"] 14 | NestedFormat = Literal["dots", "brackets"] 15 | 16 | PrimitiveData = Union[str, int, float, bool, None] 17 | # this should be Data = Union[PrimitiveData, "List[Data]", "Tuple[Data]", "Mapping[str, Data]"] 18 | # https://github.com/microsoft/pyright/issues/3555 19 | Data = Union[PrimitiveData, List[Any], Tuple[Any], "Mapping[str, Any]"] 20 | Params = Mapping[str, Data] 21 | 22 | 23 | class Querystring: 24 | array_format: ArrayFormat 25 | nested_format: NestedFormat 26 | 27 | def __init__( 28 | self, 29 | *, 30 | array_format: ArrayFormat = "repeat", 31 | nested_format: NestedFormat = "brackets", 32 | ) -> None: 33 | self.array_format = array_format 34 | self.nested_format = nested_format 35 | 36 | def parse(self, query: str) -> Mapping[str, object]: 37 | # Note: custom format syntax is not supported yet 38 | return parse_qs(query) 39 | 40 | def stringify( 41 | self, 42 | params: Params, 43 | *, 44 | array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN, 45 | nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN, 46 | ) -> str: 47 | return urlencode( 48 | self.stringify_items( 49 | params, 50 | array_format=array_format, 51 | nested_format=nested_format, 52 | ) 53 | ) 54 | 55 | def stringify_items( 56 | self, 57 | params: Params, 58 | *, 59 | array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN, 60 | nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN, 61 | ) -> list[tuple[str, str]]: 62 | opts = Options( 63 | qs=self, 64 | array_format=array_format, 65 | nested_format=nested_format, 66 | ) 67 | return flatten([self._stringify_item(key, value, opts) for key, value in params.items()]) 68 | 69 | def _stringify_item( 70 | self, 71 | key: str, 72 | value: Data, 73 | opts: Options, 74 | ) -> list[tuple[str, str]]: 75 | if isinstance(value, Mapping): 76 | items: list[tuple[str, str]] = [] 77 | nested_format = opts.nested_format 78 | for subkey, subvalue in value.items(): 79 | items.extend( 80 | self._stringify_item( 81 | # TODO: error if unknown format 82 | f"{key}.{subkey}" if nested_format == "dots" else f"{key}[{subkey}]", 83 | subvalue, 84 | opts, 85 | ) 86 | ) 87 | return items 88 | 89 | if isinstance(value, (list, tuple)): 90 | array_format = opts.array_format 91 | if array_format == "comma": 92 | return [ 93 | ( 94 | key, 95 | ",".join(self._primitive_value_to_str(item) for item in value if item is not None), 96 | ), 97 | ] 98 | elif array_format == "repeat": 99 | items = [] 100 | for item in value: 101 | items.extend(self._stringify_item(key, item, opts)) 102 | return items 103 | elif array_format == "indices": 104 | raise NotImplementedError("The array indices format is not supported yet") 105 | elif array_format == "brackets": 106 | items = [] 107 | key = key + "[]" 108 | for item in value: 109 | items.extend(self._stringify_item(key, item, opts)) 110 | return items 111 | else: 112 | raise NotImplementedError( 113 | f"Unknown array_format value: {array_format}, choose from {', '.join(get_args(ArrayFormat))}" 114 | ) 115 | 116 | serialised = self._primitive_value_to_str(value) 117 | if not serialised: 118 | return [] 119 | return [(key, serialised)] 120 | 121 | def _primitive_value_to_str(self, value: PrimitiveData) -> str: 122 | # copied from httpx 123 | if value is True: 124 | return "true" 125 | elif value is False: 126 | return "false" 127 | elif value is None: 128 | return "" 129 | return str(value) 130 | 131 | 132 | _qs = Querystring() 133 | parse = _qs.parse 134 | stringify = _qs.stringify 135 | stringify_items = _qs.stringify_items 136 | 137 | 138 | class Options: 139 | array_format: ArrayFormat 140 | nested_format: NestedFormat 141 | 142 | def __init__( 143 | self, 144 | qs: Querystring = _qs, 145 | *, 146 | array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN, 147 | nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN, 148 | ) -> None: 149 | self.array_format = qs.array_format if isinstance(array_format, NotGiven) else array_format 150 | self.nested_format = qs.nested_format if isinstance(nested_format, NotGiven) else nested_format 151 | -------------------------------------------------------------------------------- /src/retell/_resource.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | import time 6 | from typing import TYPE_CHECKING 7 | 8 | import anyio 9 | 10 | if TYPE_CHECKING: 11 | from ._client import Retell, AsyncRetell 12 | 13 | 14 | class SyncAPIResource: 15 | _client: Retell 16 | 17 | def __init__(self, client: Retell) -> None: 18 | self._client = client 19 | self._get = client.get 20 | self._post = client.post 21 | self._patch = client.patch 22 | self._put = client.put 23 | self._delete = client.delete 24 | self._get_api_list = client.get_api_list 25 | 26 | def _sleep(self, seconds: float) -> None: 27 | time.sleep(seconds) 28 | 29 | 30 | class AsyncAPIResource: 31 | _client: AsyncRetell 32 | 33 | def __init__(self, client: AsyncRetell) -> None: 34 | self._client = client 35 | self._get = client.get 36 | self._post = client.post 37 | self._patch = client.patch 38 | self._put = client.put 39 | self._delete = client.delete 40 | self._get_api_list = client.get_api_list 41 | 42 | async def _sleep(self, seconds: float) -> None: 43 | await anyio.sleep(seconds) 44 | -------------------------------------------------------------------------------- /src/retell/_types.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from os import PathLike 4 | from typing import ( 5 | IO, 6 | TYPE_CHECKING, 7 | Any, 8 | Dict, 9 | List, 10 | Type, 11 | Tuple, 12 | Union, 13 | Mapping, 14 | TypeVar, 15 | Callable, 16 | Optional, 17 | Sequence, 18 | ) 19 | from typing_extensions import Set, Literal, Protocol, TypeAlias, TypedDict, override, runtime_checkable 20 | 21 | import httpx 22 | import pydantic 23 | from httpx import URL, Proxy, Timeout, Response, BaseTransport, AsyncBaseTransport 24 | 25 | if TYPE_CHECKING: 26 | from ._models import BaseModel 27 | from ._response import APIResponse, AsyncAPIResponse 28 | 29 | Transport = BaseTransport 30 | AsyncTransport = AsyncBaseTransport 31 | Query = Mapping[str, object] 32 | Body = object 33 | AnyMapping = Mapping[str, object] 34 | ModelT = TypeVar("ModelT", bound=pydantic.BaseModel) 35 | _T = TypeVar("_T") 36 | 37 | 38 | # Approximates httpx internal ProxiesTypes and RequestFiles types 39 | # while adding support for `PathLike` instances 40 | ProxiesDict = Dict["str | URL", Union[None, str, URL, Proxy]] 41 | ProxiesTypes = Union[str, Proxy, ProxiesDict] 42 | if TYPE_CHECKING: 43 | Base64FileInput = Union[IO[bytes], PathLike[str]] 44 | FileContent = Union[IO[bytes], bytes, PathLike[str]] 45 | else: 46 | Base64FileInput = Union[IO[bytes], PathLike] 47 | FileContent = Union[IO[bytes], bytes, PathLike] # PathLike is not subscriptable in Python 3.8. 48 | FileTypes = Union[ 49 | # file (or bytes) 50 | FileContent, 51 | # (filename, file (or bytes)) 52 | Tuple[Optional[str], FileContent], 53 | # (filename, file (or bytes), content_type) 54 | Tuple[Optional[str], FileContent, Optional[str]], 55 | # (filename, file (or bytes), content_type, headers) 56 | Tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]], 57 | ] 58 | RequestFiles = Union[Mapping[str, FileTypes], Sequence[Tuple[str, FileTypes]]] 59 | 60 | # duplicate of the above but without our custom file support 61 | HttpxFileContent = Union[IO[bytes], bytes] 62 | HttpxFileTypes = Union[ 63 | # file (or bytes) 64 | HttpxFileContent, 65 | # (filename, file (or bytes)) 66 | Tuple[Optional[str], HttpxFileContent], 67 | # (filename, file (or bytes), content_type) 68 | Tuple[Optional[str], HttpxFileContent, Optional[str]], 69 | # (filename, file (or bytes), content_type, headers) 70 | Tuple[Optional[str], HttpxFileContent, Optional[str], Mapping[str, str]], 71 | ] 72 | HttpxRequestFiles = Union[Mapping[str, HttpxFileTypes], Sequence[Tuple[str, HttpxFileTypes]]] 73 | 74 | # Workaround to support (cast_to: Type[ResponseT]) -> ResponseT 75 | # where ResponseT includes `None`. In order to support directly 76 | # passing `None`, overloads would have to be defined for every 77 | # method that uses `ResponseT` which would lead to an unacceptable 78 | # amount of code duplication and make it unreadable. See _base_client.py 79 | # for example usage. 80 | # 81 | # This unfortunately means that you will either have 82 | # to import this type and pass it explicitly: 83 | # 84 | # from retell import NoneType 85 | # client.get('/foo', cast_to=NoneType) 86 | # 87 | # or build it yourself: 88 | # 89 | # client.get('/foo', cast_to=type(None)) 90 | if TYPE_CHECKING: 91 | NoneType: Type[None] 92 | else: 93 | NoneType = type(None) 94 | 95 | 96 | class RequestOptions(TypedDict, total=False): 97 | headers: Headers 98 | max_retries: int 99 | timeout: float | Timeout | None 100 | params: Query 101 | extra_json: AnyMapping 102 | idempotency_key: str 103 | follow_redirects: bool 104 | 105 | 106 | # Sentinel class used until PEP 0661 is accepted 107 | class NotGiven: 108 | """ 109 | A sentinel singleton class used to distinguish omitted keyword arguments 110 | from those passed in with the value None (which may have different behavior). 111 | 112 | For example: 113 | 114 | ```py 115 | def get(timeout: Union[int, NotGiven, None] = NotGiven()) -> Response: ... 116 | 117 | 118 | get(timeout=1) # 1s timeout 119 | get(timeout=None) # No timeout 120 | get() # Default timeout behavior, which may not be statically known at the method definition. 121 | ``` 122 | """ 123 | 124 | def __bool__(self) -> Literal[False]: 125 | return False 126 | 127 | @override 128 | def __repr__(self) -> str: 129 | return "NOT_GIVEN" 130 | 131 | 132 | NotGivenOr = Union[_T, NotGiven] 133 | NOT_GIVEN = NotGiven() 134 | 135 | 136 | class Omit: 137 | """In certain situations you need to be able to represent a case where a default value has 138 | to be explicitly removed and `None` is not an appropriate substitute, for example: 139 | 140 | ```py 141 | # as the default `Content-Type` header is `application/json` that will be sent 142 | client.post("/upload/files", files={"file": b"my raw file content"}) 143 | 144 | # you can't explicitly override the header as it has to be dynamically generated 145 | # to look something like: 'multipart/form-data; boundary=0d8382fcf5f8c3be01ca2e11002d2983' 146 | client.post(..., headers={"Content-Type": "multipart/form-data"}) 147 | 148 | # instead you can remove the default `application/json` header by passing Omit 149 | client.post(..., headers={"Content-Type": Omit()}) 150 | ``` 151 | """ 152 | 153 | def __bool__(self) -> Literal[False]: 154 | return False 155 | 156 | 157 | @runtime_checkable 158 | class ModelBuilderProtocol(Protocol): 159 | @classmethod 160 | def build( 161 | cls: type[_T], 162 | *, 163 | response: Response, 164 | data: object, 165 | ) -> _T: ... 166 | 167 | 168 | Headers = Mapping[str, Union[str, Omit]] 169 | 170 | 171 | class HeadersLikeProtocol(Protocol): 172 | def get(self, __key: str) -> str | None: ... 173 | 174 | 175 | HeadersLike = Union[Headers, HeadersLikeProtocol] 176 | 177 | ResponseT = TypeVar( 178 | "ResponseT", 179 | bound=Union[ 180 | object, 181 | str, 182 | None, 183 | "BaseModel", 184 | List[Any], 185 | Dict[str, Any], 186 | Response, 187 | ModelBuilderProtocol, 188 | "APIResponse[Any]", 189 | "AsyncAPIResponse[Any]", 190 | ], 191 | ) 192 | 193 | StrBytesIntFloat = Union[str, bytes, int, float] 194 | 195 | # Note: copied from Pydantic 196 | # https://github.com/pydantic/pydantic/blob/6f31f8f68ef011f84357330186f603ff295312fd/pydantic/main.py#L79 197 | IncEx: TypeAlias = Union[Set[int], Set[str], Mapping[int, Union["IncEx", bool]], Mapping[str, Union["IncEx", bool]]] 198 | 199 | PostParser = Callable[[Any], Any] 200 | 201 | 202 | @runtime_checkable 203 | class InheritsGeneric(Protocol): 204 | """Represents a type that has inherited from `Generic` 205 | 206 | The `__orig_bases__` property can be used to determine the resolved 207 | type variable for a given base class. 208 | """ 209 | 210 | __orig_bases__: tuple[_GenericAlias] 211 | 212 | 213 | class _GenericAlias(Protocol): 214 | __origin__: type[object] 215 | 216 | 217 | class HttpxSendArgs(TypedDict, total=False): 218 | auth: httpx.Auth 219 | follow_redirects: bool 220 | -------------------------------------------------------------------------------- /src/retell/_utils/__init__.py: -------------------------------------------------------------------------------- 1 | from ._sync import asyncify as asyncify 2 | from ._proxy import LazyProxy as LazyProxy 3 | from ._utils import ( 4 | flatten as flatten, 5 | is_dict as is_dict, 6 | is_list as is_list, 7 | is_given as is_given, 8 | is_tuple as is_tuple, 9 | json_safe as json_safe, 10 | lru_cache as lru_cache, 11 | is_mapping as is_mapping, 12 | is_tuple_t as is_tuple_t, 13 | parse_date as parse_date, 14 | is_iterable as is_iterable, 15 | is_sequence as is_sequence, 16 | coerce_float as coerce_float, 17 | is_mapping_t as is_mapping_t, 18 | removeprefix as removeprefix, 19 | removesuffix as removesuffix, 20 | extract_files as extract_files, 21 | is_sequence_t as is_sequence_t, 22 | required_args as required_args, 23 | coerce_boolean as coerce_boolean, 24 | coerce_integer as coerce_integer, 25 | file_from_path as file_from_path, 26 | parse_datetime as parse_datetime, 27 | strip_not_given as strip_not_given, 28 | deepcopy_minimal as deepcopy_minimal, 29 | get_async_library as get_async_library, 30 | maybe_coerce_float as maybe_coerce_float, 31 | get_required_header as get_required_header, 32 | maybe_coerce_boolean as maybe_coerce_boolean, 33 | maybe_coerce_integer as maybe_coerce_integer, 34 | ) 35 | from ._typing import ( 36 | is_list_type as is_list_type, 37 | is_union_type as is_union_type, 38 | extract_type_arg as extract_type_arg, 39 | is_iterable_type as is_iterable_type, 40 | is_required_type as is_required_type, 41 | is_annotated_type as is_annotated_type, 42 | is_type_alias_type as is_type_alias_type, 43 | strip_annotated_type as strip_annotated_type, 44 | extract_type_var_from_base as extract_type_var_from_base, 45 | ) 46 | from ._streams import consume_sync_iterator as consume_sync_iterator, consume_async_iterator as consume_async_iterator 47 | from ._transform import ( 48 | PropertyInfo as PropertyInfo, 49 | transform as transform, 50 | async_transform as async_transform, 51 | maybe_transform as maybe_transform, 52 | async_maybe_transform as async_maybe_transform, 53 | ) 54 | from ._reflection import ( 55 | function_has_argument as function_has_argument, 56 | assert_signatures_in_sync as assert_signatures_in_sync, 57 | ) 58 | -------------------------------------------------------------------------------- /src/retell/_utils/_logs.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | 4 | logger: logging.Logger = logging.getLogger("retell") 5 | httpx_logger: logging.Logger = logging.getLogger("httpx") 6 | 7 | 8 | def _basic_config() -> None: 9 | # e.g. [2023-10-05 14:12:26 - retell._base_client:818 - DEBUG] HTTP Request: POST http://127.0.0.1:4010/foo/bar "200 OK" 10 | logging.basicConfig( 11 | format="[%(asctime)s - %(name)s:%(lineno)d - %(levelname)s] %(message)s", 12 | datefmt="%Y-%m-%d %H:%M:%S", 13 | ) 14 | 15 | 16 | def setup_logging() -> None: 17 | env = os.environ.get("RETELL_LOG") 18 | if env == "debug": 19 | _basic_config() 20 | logger.setLevel(logging.DEBUG) 21 | httpx_logger.setLevel(logging.DEBUG) 22 | elif env == "info": 23 | _basic_config() 24 | logger.setLevel(logging.INFO) 25 | httpx_logger.setLevel(logging.INFO) 26 | -------------------------------------------------------------------------------- /src/retell/_utils/_proxy.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC, abstractmethod 4 | from typing import Generic, TypeVar, Iterable, cast 5 | from typing_extensions import override 6 | 7 | T = TypeVar("T") 8 | 9 | 10 | class LazyProxy(Generic[T], ABC): 11 | """Implements data methods to pretend that an instance is another instance. 12 | 13 | This includes forwarding attribute access and other methods. 14 | """ 15 | 16 | # Note: we have to special case proxies that themselves return proxies 17 | # to support using a proxy as a catch-all for any random access, e.g. `proxy.foo.bar.baz` 18 | 19 | def __getattr__(self, attr: str) -> object: 20 | proxied = self.__get_proxied__() 21 | if isinstance(proxied, LazyProxy): 22 | return proxied # pyright: ignore 23 | return getattr(proxied, attr) 24 | 25 | @override 26 | def __repr__(self) -> str: 27 | proxied = self.__get_proxied__() 28 | if isinstance(proxied, LazyProxy): 29 | return proxied.__class__.__name__ 30 | return repr(self.__get_proxied__()) 31 | 32 | @override 33 | def __str__(self) -> str: 34 | proxied = self.__get_proxied__() 35 | if isinstance(proxied, LazyProxy): 36 | return proxied.__class__.__name__ 37 | return str(proxied) 38 | 39 | @override 40 | def __dir__(self) -> Iterable[str]: 41 | proxied = self.__get_proxied__() 42 | if isinstance(proxied, LazyProxy): 43 | return [] 44 | return proxied.__dir__() 45 | 46 | @property # type: ignore 47 | @override 48 | def __class__(self) -> type: # pyright: ignore 49 | try: 50 | proxied = self.__get_proxied__() 51 | except Exception: 52 | return type(self) 53 | if issubclass(type(proxied), LazyProxy): 54 | return type(proxied) 55 | return proxied.__class__ 56 | 57 | def __get_proxied__(self) -> T: 58 | return self.__load__() 59 | 60 | def __as_proxied__(self) -> T: 61 | """Helper method that returns the current proxy, typed as the loaded object""" 62 | return cast(T, self) 63 | 64 | @abstractmethod 65 | def __load__(self) -> T: ... 66 | -------------------------------------------------------------------------------- /src/retell/_utils/_reflection.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import inspect 4 | from typing import Any, Callable 5 | 6 | 7 | def function_has_argument(func: Callable[..., Any], arg_name: str) -> bool: 8 | """Returns whether or not the given function has a specific parameter""" 9 | sig = inspect.signature(func) 10 | return arg_name in sig.parameters 11 | 12 | 13 | def assert_signatures_in_sync( 14 | source_func: Callable[..., Any], 15 | check_func: Callable[..., Any], 16 | *, 17 | exclude_params: set[str] = set(), 18 | ) -> None: 19 | """Ensure that the signature of the second function matches the first.""" 20 | 21 | check_sig = inspect.signature(check_func) 22 | source_sig = inspect.signature(source_func) 23 | 24 | errors: list[str] = [] 25 | 26 | for name, source_param in source_sig.parameters.items(): 27 | if name in exclude_params: 28 | continue 29 | 30 | custom_param = check_sig.parameters.get(name) 31 | if not custom_param: 32 | errors.append(f"the `{name}` param is missing") 33 | continue 34 | 35 | if custom_param.annotation != source_param.annotation: 36 | errors.append( 37 | f"types for the `{name}` param are do not match; source={repr(source_param.annotation)} checking={repr(custom_param.annotation)}" 38 | ) 39 | continue 40 | 41 | if errors: 42 | raise AssertionError(f"{len(errors)} errors encountered when comparing signatures:\n\n" + "\n\n".join(errors)) 43 | -------------------------------------------------------------------------------- /src/retell/_utils/_resources_proxy.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any 4 | from typing_extensions import override 5 | 6 | from ._proxy import LazyProxy 7 | 8 | 9 | class ResourcesProxy(LazyProxy[Any]): 10 | """A proxy for the `retell.resources` module. 11 | 12 | This is used so that we can lazily import `retell.resources` only when 13 | needed *and* so that users can just import `retell` and reference `retell.resources` 14 | """ 15 | 16 | @override 17 | def __load__(self) -> Any: 18 | import importlib 19 | 20 | mod = importlib.import_module("retell.resources") 21 | return mod 22 | 23 | 24 | resources = ResourcesProxy().__as_proxied__() 25 | -------------------------------------------------------------------------------- /src/retell/_utils/_streams.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from typing_extensions import Iterator, AsyncIterator 3 | 4 | 5 | def consume_sync_iterator(iterator: Iterator[Any]) -> None: 6 | for _ in iterator: 7 | ... 8 | 9 | 10 | async def consume_async_iterator(iterator: AsyncIterator[Any]) -> None: 11 | async for _ in iterator: 12 | ... 13 | -------------------------------------------------------------------------------- /src/retell/_utils/_sync.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | import asyncio 5 | import functools 6 | import contextvars 7 | from typing import Any, TypeVar, Callable, Awaitable 8 | from typing_extensions import ParamSpec 9 | 10 | import anyio 11 | import sniffio 12 | import anyio.to_thread 13 | 14 | T_Retval = TypeVar("T_Retval") 15 | T_ParamSpec = ParamSpec("T_ParamSpec") 16 | 17 | 18 | if sys.version_info >= (3, 9): 19 | _asyncio_to_thread = asyncio.to_thread 20 | else: 21 | # backport of https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread 22 | # for Python 3.8 support 23 | async def _asyncio_to_thread( 24 | func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs 25 | ) -> Any: 26 | """Asynchronously run function *func* in a separate thread. 27 | 28 | Any *args and **kwargs supplied for this function are directly passed 29 | to *func*. Also, the current :class:`contextvars.Context` is propagated, 30 | allowing context variables from the main thread to be accessed in the 31 | separate thread. 32 | 33 | Returns a coroutine that can be awaited to get the eventual result of *func*. 34 | """ 35 | loop = asyncio.events.get_running_loop() 36 | ctx = contextvars.copy_context() 37 | func_call = functools.partial(ctx.run, func, *args, **kwargs) 38 | return await loop.run_in_executor(None, func_call) 39 | 40 | 41 | async def to_thread( 42 | func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs 43 | ) -> T_Retval: 44 | if sniffio.current_async_library() == "asyncio": 45 | return await _asyncio_to_thread(func, *args, **kwargs) 46 | 47 | return await anyio.to_thread.run_sync( 48 | functools.partial(func, *args, **kwargs), 49 | ) 50 | 51 | 52 | # inspired by `asyncer`, https://github.com/tiangolo/asyncer 53 | def asyncify(function: Callable[T_ParamSpec, T_Retval]) -> Callable[T_ParamSpec, Awaitable[T_Retval]]: 54 | """ 55 | Take a blocking function and create an async one that receives the same 56 | positional and keyword arguments. For python version 3.9 and above, it uses 57 | asyncio.to_thread to run the function in a separate thread. For python version 58 | 3.8, it uses locally defined copy of the asyncio.to_thread function which was 59 | introduced in python 3.9. 60 | 61 | Usage: 62 | 63 | ```python 64 | def blocking_func(arg1, arg2, kwarg1=None): 65 | # blocking code 66 | return result 67 | 68 | 69 | result = asyncify(blocking_function)(arg1, arg2, kwarg1=value1) 70 | ``` 71 | 72 | ## Arguments 73 | 74 | `function`: a blocking regular callable (e.g. a function) 75 | 76 | ## Return 77 | 78 | An async function that takes the same positional and keyword arguments as the 79 | original one, that when called runs the same original function in a thread worker 80 | and returns the result. 81 | """ 82 | 83 | async def wrapper(*args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs) -> T_Retval: 84 | return await to_thread(function, *args, **kwargs) 85 | 86 | return wrapper 87 | -------------------------------------------------------------------------------- /src/retell/_utils/_typing.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | import typing 5 | import typing_extensions 6 | from typing import Any, TypeVar, Iterable, cast 7 | from collections import abc as _c_abc 8 | from typing_extensions import ( 9 | TypeIs, 10 | Required, 11 | Annotated, 12 | get_args, 13 | get_origin, 14 | ) 15 | 16 | from ._utils import lru_cache 17 | from .._types import InheritsGeneric 18 | from .._compat import is_union as _is_union 19 | 20 | 21 | def is_annotated_type(typ: type) -> bool: 22 | return get_origin(typ) == Annotated 23 | 24 | 25 | def is_list_type(typ: type) -> bool: 26 | return (get_origin(typ) or typ) == list 27 | 28 | 29 | def is_iterable_type(typ: type) -> bool: 30 | """If the given type is `typing.Iterable[T]`""" 31 | origin = get_origin(typ) or typ 32 | return origin == Iterable or origin == _c_abc.Iterable 33 | 34 | 35 | def is_union_type(typ: type) -> bool: 36 | return _is_union(get_origin(typ)) 37 | 38 | 39 | def is_required_type(typ: type) -> bool: 40 | return get_origin(typ) == Required 41 | 42 | 43 | def is_typevar(typ: type) -> bool: 44 | # type ignore is required because type checkers 45 | # think this expression will always return False 46 | return type(typ) == TypeVar # type: ignore 47 | 48 | 49 | _TYPE_ALIAS_TYPES: tuple[type[typing_extensions.TypeAliasType], ...] = (typing_extensions.TypeAliasType,) 50 | if sys.version_info >= (3, 12): 51 | _TYPE_ALIAS_TYPES = (*_TYPE_ALIAS_TYPES, typing.TypeAliasType) 52 | 53 | 54 | def is_type_alias_type(tp: Any, /) -> TypeIs[typing_extensions.TypeAliasType]: 55 | """Return whether the provided argument is an instance of `TypeAliasType`. 56 | 57 | ```python 58 | type Int = int 59 | is_type_alias_type(Int) 60 | # > True 61 | Str = TypeAliasType("Str", str) 62 | is_type_alias_type(Str) 63 | # > True 64 | ``` 65 | """ 66 | return isinstance(tp, _TYPE_ALIAS_TYPES) 67 | 68 | 69 | # Extracts T from Annotated[T, ...] or from Required[Annotated[T, ...]] 70 | @lru_cache(maxsize=8096) 71 | def strip_annotated_type(typ: type) -> type: 72 | if is_required_type(typ) or is_annotated_type(typ): 73 | return strip_annotated_type(cast(type, get_args(typ)[0])) 74 | 75 | return typ 76 | 77 | 78 | def extract_type_arg(typ: type, index: int) -> type: 79 | args = get_args(typ) 80 | try: 81 | return cast(type, args[index]) 82 | except IndexError as err: 83 | raise RuntimeError(f"Expected type {typ} to have a type argument at index {index} but it did not") from err 84 | 85 | 86 | def extract_type_var_from_base( 87 | typ: type, 88 | *, 89 | generic_bases: tuple[type, ...], 90 | index: int, 91 | failure_message: str | None = None, 92 | ) -> type: 93 | """Given a type like `Foo[T]`, returns the generic type variable `T`. 94 | 95 | This also handles the case where a concrete subclass is given, e.g. 96 | ```py 97 | class MyResponse(Foo[bytes]): 98 | ... 99 | 100 | extract_type_var(MyResponse, bases=(Foo,), index=0) -> bytes 101 | ``` 102 | 103 | And where a generic subclass is given: 104 | ```py 105 | _T = TypeVar('_T') 106 | class MyResponse(Foo[_T]): 107 | ... 108 | 109 | extract_type_var(MyResponse[bytes], bases=(Foo,), index=0) -> bytes 110 | ``` 111 | """ 112 | cls = cast(object, get_origin(typ) or typ) 113 | if cls in generic_bases: # pyright: ignore[reportUnnecessaryContains] 114 | # we're given the class directly 115 | return extract_type_arg(typ, index) 116 | 117 | # if a subclass is given 118 | # --- 119 | # this is needed as __orig_bases__ is not present in the typeshed stubs 120 | # because it is intended to be for internal use only, however there does 121 | # not seem to be a way to resolve generic TypeVars for inherited subclasses 122 | # without using it. 123 | if isinstance(cls, InheritsGeneric): 124 | target_base_class: Any | None = None 125 | for base in cls.__orig_bases__: 126 | if base.__origin__ in generic_bases: 127 | target_base_class = base 128 | break 129 | 130 | if target_base_class is None: 131 | raise RuntimeError( 132 | "Could not find the generic base class;\n" 133 | "This should never happen;\n" 134 | f"Does {cls} inherit from one of {generic_bases} ?" 135 | ) 136 | 137 | extracted = extract_type_arg(target_base_class, index) 138 | if is_typevar(extracted): 139 | # If the extracted type argument is itself a type variable 140 | # then that means the subclass itself is generic, so we have 141 | # to resolve the type argument from the class itself, not 142 | # the base class. 143 | # 144 | # Note: if there is more than 1 type argument, the subclass could 145 | # change the ordering of the type arguments, this is not currently 146 | # supported. 147 | return extract_type_arg(typ, index) 148 | 149 | return extracted 150 | 151 | raise RuntimeError(failure_message or f"Could not resolve inner type variable at index {index} for {typ}") 152 | -------------------------------------------------------------------------------- /src/retell/_version.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | __title__ = "retell" 4 | __version__ = "4.33.0" # x-release-please-version 5 | -------------------------------------------------------------------------------- /src/retell/lib/.keep: -------------------------------------------------------------------------------- 1 | File generated from our OpenAPI spec by Stainless. 2 | 3 | This directory can be used to store custom files to expand the SDK. 4 | It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. -------------------------------------------------------------------------------- /src/retell/lib/__init__.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from .webhook_auth import verify as verify # type: ignore 6 | 7 | __all__ = ["verify"] 8 | -------------------------------------------------------------------------------- /src/retell/lib/webhook_auth.py: -------------------------------------------------------------------------------- 1 | import re 2 | import hmac 3 | import time 4 | import base64 5 | import hashlib 6 | 7 | from cryptography.exceptions import InvalidSignature # type: ignore 8 | from cryptography.hazmat.primitives import hashes # type: ignore 9 | from cryptography.hazmat.primitives.asymmetric import padding # type: ignore 10 | from cryptography.hazmat.primitives.serialization import load_pem_public_key, load_pem_private_key # type: ignore 11 | 12 | FIVE_MINUTES = 5 * 60 * 1000 13 | 14 | 15 | def make_secure_webhooks(get_signer, get_verifier): # type: ignore 16 | def sign(input_str, secret_or_private_key, timestamp=None): # type: ignore 17 | if timestamp is None: 18 | timestamp = int(time.time() * 1000) 19 | signer = get_signer(secret_or_private_key) # type: ignore 20 | return f"v={timestamp},d={signer(input_str + str(timestamp))}" # type: ignore 21 | 22 | def verify(input_str, secret_or_public_key, signature, opts=None): # type: ignore 23 | if opts is None: 24 | opts = {} 25 | match = re.search(r"v=(\d+),d=(.*)", signature) # type: ignore 26 | if not match: 27 | return False 28 | poststamp = int(match.group(1)) 29 | post_digest = match.group(2) 30 | timestamp = opts.get("timestamp", int(time.time() * 1000)) # type: ignore 31 | timeout = opts.get("timeout", FIVE_MINUTES) # type: ignore 32 | difference = abs(timestamp - poststamp) # type: ignore 33 | if difference > timeout: 34 | return False 35 | verifier = get_verifier(secret_or_public_key) # type: ignore 36 | return verifier(input_str + str(poststamp), post_digest) # type: ignore 37 | 38 | return {"sign": sign, "verify": verify} # type: ignore 39 | 40 | 41 | def symmetric_get_signer(secret): # type: ignore 42 | def signer(input_str): # type: ignore 43 | h = hmac.new(secret.encode(), digestmod=hashlib.sha256) # type: ignore 44 | h.update(input_str.encode()) # type: ignore 45 | return h.hexdigest() # type: ignore 46 | 47 | return signer # type: ignore 48 | 49 | 50 | def symmetric_get_verifier(secret): # type: ignore 51 | def verifier(input_str, digest): # type: ignore 52 | algo = hashlib.sha256 53 | hmac_instance = hmac.new(secret.encode(), digestmod=algo) # type: ignore 54 | hmac_instance.update(input_str.encode()) # type: ignore 55 | return hmac_instance.hexdigest() == digest # type: ignore 56 | 57 | return verifier # type: ignore 58 | 59 | 60 | def asymmetric_get_signer(private_key): # type: ignore 61 | def signer(input_str): # type: ignore 62 | private_key_obj = load_pem_private_key(private_key.encode(), password=None) # type: ignore 63 | signature = private_key_obj.sign( # type: ignore 64 | input_str.encode(), # type: ignore 65 | padding.PSS( # type: ignore 66 | mgf=padding.MGF1(hashes.SHA256()), # type: ignore 67 | salt_length=padding.PSS.MAX_LENGTH, # type: ignore 68 | ), 69 | hashes.SHA256(), # type: ignore 70 | ) 71 | return base64.b64encode(signature).decode() # type: ignore 72 | 73 | return signer # type: ignore 74 | 75 | 76 | def asymmetric_get_verifier(public_key): # type: ignore 77 | def verifier(input_str, digest): # type: ignore 78 | public_key_obj = load_pem_public_key(public_key.encode()) # type: ignore 79 | try: 80 | public_key_obj.verify( # type: ignore 81 | base64.b64decode(digest), # type: ignore 82 | input_str.encode(), # type: ignore 83 | padding.PSS( # type: ignore 84 | mgf=padding.MGF1(hashes.SHA256()), # type: ignore 85 | salt_length=padding.PSS.MAX_LENGTH, # type: ignore 86 | ), 87 | hashes.SHA256(), # type: ignore 88 | ) 89 | return True 90 | except InvalidSignature: 91 | return False 92 | 93 | return verifier # type: ignore 94 | 95 | 96 | symmetric = make_secure_webhooks(symmetric_get_signer, symmetric_get_verifier) # type: ignore 97 | 98 | 99 | def verify(body, api_key, signature): # type: ignore 100 | return symmetric["verify"](body, api_key, signature) # type: ignore 101 | -------------------------------------------------------------------------------- /src/retell/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RetellAI/retell-python-sdk/825954d27578a32922445da21f6edf532b3d3774/src/retell/py.typed -------------------------------------------------------------------------------- /src/retell/resources/__init__.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from .llm import ( 4 | LlmResource, 5 | AsyncLlmResource, 6 | LlmResourceWithRawResponse, 7 | AsyncLlmResourceWithRawResponse, 8 | LlmResourceWithStreamingResponse, 9 | AsyncLlmResourceWithStreamingResponse, 10 | ) 11 | from .call import ( 12 | CallResource, 13 | AsyncCallResource, 14 | CallResourceWithRawResponse, 15 | AsyncCallResourceWithRawResponse, 16 | CallResourceWithStreamingResponse, 17 | AsyncCallResourceWithStreamingResponse, 18 | ) 19 | from .chat import ( 20 | ChatResource, 21 | AsyncChatResource, 22 | ChatResourceWithRawResponse, 23 | AsyncChatResourceWithRawResponse, 24 | ChatResourceWithStreamingResponse, 25 | AsyncChatResourceWithStreamingResponse, 26 | ) 27 | from .agent import ( 28 | AgentResource, 29 | AsyncAgentResource, 30 | AgentResourceWithRawResponse, 31 | AsyncAgentResourceWithRawResponse, 32 | AgentResourceWithStreamingResponse, 33 | AsyncAgentResourceWithStreamingResponse, 34 | ) 35 | from .voice import ( 36 | VoiceResource, 37 | AsyncVoiceResource, 38 | VoiceResourceWithRawResponse, 39 | AsyncVoiceResourceWithRawResponse, 40 | VoiceResourceWithStreamingResponse, 41 | AsyncVoiceResourceWithStreamingResponse, 42 | ) 43 | from .batch_call import ( 44 | BatchCallResource, 45 | AsyncBatchCallResource, 46 | BatchCallResourceWithRawResponse, 47 | AsyncBatchCallResourceWithRawResponse, 48 | BatchCallResourceWithStreamingResponse, 49 | AsyncBatchCallResourceWithStreamingResponse, 50 | ) 51 | from .concurrency import ( 52 | ConcurrencyResource, 53 | AsyncConcurrencyResource, 54 | ConcurrencyResourceWithRawResponse, 55 | AsyncConcurrencyResourceWithRawResponse, 56 | ConcurrencyResourceWithStreamingResponse, 57 | AsyncConcurrencyResourceWithStreamingResponse, 58 | ) 59 | from .phone_number import ( 60 | PhoneNumberResource, 61 | AsyncPhoneNumberResource, 62 | PhoneNumberResourceWithRawResponse, 63 | AsyncPhoneNumberResourceWithRawResponse, 64 | PhoneNumberResourceWithStreamingResponse, 65 | AsyncPhoneNumberResourceWithStreamingResponse, 66 | ) 67 | from .knowledge_base import ( 68 | KnowledgeBaseResource, 69 | AsyncKnowledgeBaseResource, 70 | KnowledgeBaseResourceWithRawResponse, 71 | AsyncKnowledgeBaseResourceWithRawResponse, 72 | KnowledgeBaseResourceWithStreamingResponse, 73 | AsyncKnowledgeBaseResourceWithStreamingResponse, 74 | ) 75 | 76 | __all__ = [ 77 | "CallResource", 78 | "AsyncCallResource", 79 | "CallResourceWithRawResponse", 80 | "AsyncCallResourceWithRawResponse", 81 | "CallResourceWithStreamingResponse", 82 | "AsyncCallResourceWithStreamingResponse", 83 | "ChatResource", 84 | "AsyncChatResource", 85 | "ChatResourceWithRawResponse", 86 | "AsyncChatResourceWithRawResponse", 87 | "ChatResourceWithStreamingResponse", 88 | "AsyncChatResourceWithStreamingResponse", 89 | "PhoneNumberResource", 90 | "AsyncPhoneNumberResource", 91 | "PhoneNumberResourceWithRawResponse", 92 | "AsyncPhoneNumberResourceWithRawResponse", 93 | "PhoneNumberResourceWithStreamingResponse", 94 | "AsyncPhoneNumberResourceWithStreamingResponse", 95 | "AgentResource", 96 | "AsyncAgentResource", 97 | "AgentResourceWithRawResponse", 98 | "AsyncAgentResourceWithRawResponse", 99 | "AgentResourceWithStreamingResponse", 100 | "AsyncAgentResourceWithStreamingResponse", 101 | "LlmResource", 102 | "AsyncLlmResource", 103 | "LlmResourceWithRawResponse", 104 | "AsyncLlmResourceWithRawResponse", 105 | "LlmResourceWithStreamingResponse", 106 | "AsyncLlmResourceWithStreamingResponse", 107 | "KnowledgeBaseResource", 108 | "AsyncKnowledgeBaseResource", 109 | "KnowledgeBaseResourceWithRawResponse", 110 | "AsyncKnowledgeBaseResourceWithRawResponse", 111 | "KnowledgeBaseResourceWithStreamingResponse", 112 | "AsyncKnowledgeBaseResourceWithStreamingResponse", 113 | "VoiceResource", 114 | "AsyncVoiceResource", 115 | "VoiceResourceWithRawResponse", 116 | "AsyncVoiceResourceWithRawResponse", 117 | "VoiceResourceWithStreamingResponse", 118 | "AsyncVoiceResourceWithStreamingResponse", 119 | "ConcurrencyResource", 120 | "AsyncConcurrencyResource", 121 | "ConcurrencyResourceWithRawResponse", 122 | "AsyncConcurrencyResourceWithRawResponse", 123 | "ConcurrencyResourceWithStreamingResponse", 124 | "AsyncConcurrencyResourceWithStreamingResponse", 125 | "BatchCallResource", 126 | "AsyncBatchCallResource", 127 | "BatchCallResourceWithRawResponse", 128 | "AsyncBatchCallResourceWithRawResponse", 129 | "BatchCallResourceWithStreamingResponse", 130 | "AsyncBatchCallResourceWithStreamingResponse", 131 | ] 132 | -------------------------------------------------------------------------------- /src/retell/resources/batch_call.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from typing import Iterable 6 | 7 | import httpx 8 | 9 | from ..types import batch_call_create_batch_call_params 10 | from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven 11 | from .._utils import maybe_transform, async_maybe_transform 12 | from .._compat import cached_property 13 | from .._resource import SyncAPIResource, AsyncAPIResource 14 | from .._response import ( 15 | to_raw_response_wrapper, 16 | to_streamed_response_wrapper, 17 | async_to_raw_response_wrapper, 18 | async_to_streamed_response_wrapper, 19 | ) 20 | from .._base_client import make_request_options 21 | from ..types.batch_call_response import BatchCallResponse 22 | 23 | __all__ = ["BatchCallResource", "AsyncBatchCallResource"] 24 | 25 | 26 | class BatchCallResource(SyncAPIResource): 27 | @cached_property 28 | def with_raw_response(self) -> BatchCallResourceWithRawResponse: 29 | """ 30 | This property can be used as a prefix for any HTTP method call to return 31 | the raw response object instead of the parsed content. 32 | 33 | For more information, see https://www.github.com/RetellAI/retell-python-sdk#accessing-raw-response-data-eg-headers 34 | """ 35 | return BatchCallResourceWithRawResponse(self) 36 | 37 | @cached_property 38 | def with_streaming_response(self) -> BatchCallResourceWithStreamingResponse: 39 | """ 40 | An alternative to `.with_raw_response` that doesn't eagerly read the response body. 41 | 42 | For more information, see https://www.github.com/RetellAI/retell-python-sdk#with_streaming_response 43 | """ 44 | return BatchCallResourceWithStreamingResponse(self) 45 | 46 | def create_batch_call( 47 | self, 48 | *, 49 | from_number: str, 50 | tasks: Iterable[batch_call_create_batch_call_params.Task], 51 | name: str | NotGiven = NOT_GIVEN, 52 | trigger_timestamp: float | NotGiven = NOT_GIVEN, 53 | # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. 54 | # The extra values given here take precedence over values defined on the client or passed to this method. 55 | extra_headers: Headers | None = None, 56 | extra_query: Query | None = None, 57 | extra_body: Body | None = None, 58 | timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, 59 | ) -> BatchCallResponse: 60 | """Create a batch call 61 | 62 | Args: 63 | from_number: The number you own in E.164 format. 64 | 65 | Must be a number purchased from Retell or 66 | imported to Retell. 67 | 68 | tasks: A list of individual call tasks to be executed as part of the batch call. Each 69 | task represents a single outbound call and includes details such as the 70 | recipient's phone number and optional dynamic variables to personalize the call 71 | content. 72 | 73 | name: The name of the batch call. Only used for your own reference. 74 | 75 | trigger_timestamp: The scheduled time for sending the batch call, represented as a Unix timestamp 76 | in milliseconds. If omitted, the call will be sent immediately. 77 | 78 | extra_headers: Send extra headers 79 | 80 | extra_query: Add additional query parameters to the request 81 | 82 | extra_body: Add additional JSON properties to the request 83 | 84 | timeout: Override the client-level default timeout for this request, in seconds 85 | """ 86 | return self._post( 87 | "/create-batch-call", 88 | body=maybe_transform( 89 | { 90 | "from_number": from_number, 91 | "tasks": tasks, 92 | "name": name, 93 | "trigger_timestamp": trigger_timestamp, 94 | }, 95 | batch_call_create_batch_call_params.BatchCallCreateBatchCallParams, 96 | ), 97 | options=make_request_options( 98 | extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout 99 | ), 100 | cast_to=BatchCallResponse, 101 | ) 102 | 103 | 104 | class AsyncBatchCallResource(AsyncAPIResource): 105 | @cached_property 106 | def with_raw_response(self) -> AsyncBatchCallResourceWithRawResponse: 107 | """ 108 | This property can be used as a prefix for any HTTP method call to return 109 | the raw response object instead of the parsed content. 110 | 111 | For more information, see https://www.github.com/RetellAI/retell-python-sdk#accessing-raw-response-data-eg-headers 112 | """ 113 | return AsyncBatchCallResourceWithRawResponse(self) 114 | 115 | @cached_property 116 | def with_streaming_response(self) -> AsyncBatchCallResourceWithStreamingResponse: 117 | """ 118 | An alternative to `.with_raw_response` that doesn't eagerly read the response body. 119 | 120 | For more information, see https://www.github.com/RetellAI/retell-python-sdk#with_streaming_response 121 | """ 122 | return AsyncBatchCallResourceWithStreamingResponse(self) 123 | 124 | async def create_batch_call( 125 | self, 126 | *, 127 | from_number: str, 128 | tasks: Iterable[batch_call_create_batch_call_params.Task], 129 | name: str | NotGiven = NOT_GIVEN, 130 | trigger_timestamp: float | NotGiven = NOT_GIVEN, 131 | # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. 132 | # The extra values given here take precedence over values defined on the client or passed to this method. 133 | extra_headers: Headers | None = None, 134 | extra_query: Query | None = None, 135 | extra_body: Body | None = None, 136 | timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, 137 | ) -> BatchCallResponse: 138 | """Create a batch call 139 | 140 | Args: 141 | from_number: The number you own in E.164 format. 142 | 143 | Must be a number purchased from Retell or 144 | imported to Retell. 145 | 146 | tasks: A list of individual call tasks to be executed as part of the batch call. Each 147 | task represents a single outbound call and includes details such as the 148 | recipient's phone number and optional dynamic variables to personalize the call 149 | content. 150 | 151 | name: The name of the batch call. Only used for your own reference. 152 | 153 | trigger_timestamp: The scheduled time for sending the batch call, represented as a Unix timestamp 154 | in milliseconds. If omitted, the call will be sent immediately. 155 | 156 | extra_headers: Send extra headers 157 | 158 | extra_query: Add additional query parameters to the request 159 | 160 | extra_body: Add additional JSON properties to the request 161 | 162 | timeout: Override the client-level default timeout for this request, in seconds 163 | """ 164 | return await self._post( 165 | "/create-batch-call", 166 | body=await async_maybe_transform( 167 | { 168 | "from_number": from_number, 169 | "tasks": tasks, 170 | "name": name, 171 | "trigger_timestamp": trigger_timestamp, 172 | }, 173 | batch_call_create_batch_call_params.BatchCallCreateBatchCallParams, 174 | ), 175 | options=make_request_options( 176 | extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout 177 | ), 178 | cast_to=BatchCallResponse, 179 | ) 180 | 181 | 182 | class BatchCallResourceWithRawResponse: 183 | def __init__(self, batch_call: BatchCallResource) -> None: 184 | self._batch_call = batch_call 185 | 186 | self.create_batch_call = to_raw_response_wrapper( 187 | batch_call.create_batch_call, 188 | ) 189 | 190 | 191 | class AsyncBatchCallResourceWithRawResponse: 192 | def __init__(self, batch_call: AsyncBatchCallResource) -> None: 193 | self._batch_call = batch_call 194 | 195 | self.create_batch_call = async_to_raw_response_wrapper( 196 | batch_call.create_batch_call, 197 | ) 198 | 199 | 200 | class BatchCallResourceWithStreamingResponse: 201 | def __init__(self, batch_call: BatchCallResource) -> None: 202 | self._batch_call = batch_call 203 | 204 | self.create_batch_call = to_streamed_response_wrapper( 205 | batch_call.create_batch_call, 206 | ) 207 | 208 | 209 | class AsyncBatchCallResourceWithStreamingResponse: 210 | def __init__(self, batch_call: AsyncBatchCallResource) -> None: 211 | self._batch_call = batch_call 212 | 213 | self.create_batch_call = async_to_streamed_response_wrapper( 214 | batch_call.create_batch_call, 215 | ) 216 | -------------------------------------------------------------------------------- /src/retell/resources/concurrency.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | import httpx 6 | 7 | from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven 8 | from .._compat import cached_property 9 | from .._resource import SyncAPIResource, AsyncAPIResource 10 | from .._response import ( 11 | to_raw_response_wrapper, 12 | to_streamed_response_wrapper, 13 | async_to_raw_response_wrapper, 14 | async_to_streamed_response_wrapper, 15 | ) 16 | from .._base_client import make_request_options 17 | from ..types.concurrency_retrieve_response import ConcurrencyRetrieveResponse 18 | 19 | __all__ = ["ConcurrencyResource", "AsyncConcurrencyResource"] 20 | 21 | 22 | class ConcurrencyResource(SyncAPIResource): 23 | @cached_property 24 | def with_raw_response(self) -> ConcurrencyResourceWithRawResponse: 25 | """ 26 | This property can be used as a prefix for any HTTP method call to return 27 | the raw response object instead of the parsed content. 28 | 29 | For more information, see https://www.github.com/RetellAI/retell-python-sdk#accessing-raw-response-data-eg-headers 30 | """ 31 | return ConcurrencyResourceWithRawResponse(self) 32 | 33 | @cached_property 34 | def with_streaming_response(self) -> ConcurrencyResourceWithStreamingResponse: 35 | """ 36 | An alternative to `.with_raw_response` that doesn't eagerly read the response body. 37 | 38 | For more information, see https://www.github.com/RetellAI/retell-python-sdk#with_streaming_response 39 | """ 40 | return ConcurrencyResourceWithStreamingResponse(self) 41 | 42 | def retrieve( 43 | self, 44 | *, 45 | # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. 46 | # The extra values given here take precedence over values defined on the client or passed to this method. 47 | extra_headers: Headers | None = None, 48 | extra_query: Query | None = None, 49 | extra_body: Body | None = None, 50 | timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, 51 | ) -> ConcurrencyRetrieveResponse: 52 | """Get the current concurrency and concurrency limit of the org""" 53 | return self._get( 54 | "/get-concurrency", 55 | options=make_request_options( 56 | extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout 57 | ), 58 | cast_to=ConcurrencyRetrieveResponse, 59 | ) 60 | 61 | 62 | class AsyncConcurrencyResource(AsyncAPIResource): 63 | @cached_property 64 | def with_raw_response(self) -> AsyncConcurrencyResourceWithRawResponse: 65 | """ 66 | This property can be used as a prefix for any HTTP method call to return 67 | the raw response object instead of the parsed content. 68 | 69 | For more information, see https://www.github.com/RetellAI/retell-python-sdk#accessing-raw-response-data-eg-headers 70 | """ 71 | return AsyncConcurrencyResourceWithRawResponse(self) 72 | 73 | @cached_property 74 | def with_streaming_response(self) -> AsyncConcurrencyResourceWithStreamingResponse: 75 | """ 76 | An alternative to `.with_raw_response` that doesn't eagerly read the response body. 77 | 78 | For more information, see https://www.github.com/RetellAI/retell-python-sdk#with_streaming_response 79 | """ 80 | return AsyncConcurrencyResourceWithStreamingResponse(self) 81 | 82 | async def retrieve( 83 | self, 84 | *, 85 | # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. 86 | # The extra values given here take precedence over values defined on the client or passed to this method. 87 | extra_headers: Headers | None = None, 88 | extra_query: Query | None = None, 89 | extra_body: Body | None = None, 90 | timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, 91 | ) -> ConcurrencyRetrieveResponse: 92 | """Get the current concurrency and concurrency limit of the org""" 93 | return await self._get( 94 | "/get-concurrency", 95 | options=make_request_options( 96 | extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout 97 | ), 98 | cast_to=ConcurrencyRetrieveResponse, 99 | ) 100 | 101 | 102 | class ConcurrencyResourceWithRawResponse: 103 | def __init__(self, concurrency: ConcurrencyResource) -> None: 104 | self._concurrency = concurrency 105 | 106 | self.retrieve = to_raw_response_wrapper( 107 | concurrency.retrieve, 108 | ) 109 | 110 | 111 | class AsyncConcurrencyResourceWithRawResponse: 112 | def __init__(self, concurrency: AsyncConcurrencyResource) -> None: 113 | self._concurrency = concurrency 114 | 115 | self.retrieve = async_to_raw_response_wrapper( 116 | concurrency.retrieve, 117 | ) 118 | 119 | 120 | class ConcurrencyResourceWithStreamingResponse: 121 | def __init__(self, concurrency: ConcurrencyResource) -> None: 122 | self._concurrency = concurrency 123 | 124 | self.retrieve = to_streamed_response_wrapper( 125 | concurrency.retrieve, 126 | ) 127 | 128 | 129 | class AsyncConcurrencyResourceWithStreamingResponse: 130 | def __init__(self, concurrency: AsyncConcurrencyResource) -> None: 131 | self._concurrency = concurrency 132 | 133 | self.retrieve = async_to_streamed_response_wrapper( 134 | concurrency.retrieve, 135 | ) 136 | -------------------------------------------------------------------------------- /src/retell/resources/voice.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | import httpx 6 | 7 | from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven 8 | from .._compat import cached_property 9 | from .._resource import SyncAPIResource, AsyncAPIResource 10 | from .._response import ( 11 | to_raw_response_wrapper, 12 | to_streamed_response_wrapper, 13 | async_to_raw_response_wrapper, 14 | async_to_streamed_response_wrapper, 15 | ) 16 | from .._base_client import make_request_options 17 | from ..types.voice_response import VoiceResponse 18 | from ..types.voice_list_response import VoiceListResponse 19 | 20 | __all__ = ["VoiceResource", "AsyncVoiceResource"] 21 | 22 | 23 | class VoiceResource(SyncAPIResource): 24 | @cached_property 25 | def with_raw_response(self) -> VoiceResourceWithRawResponse: 26 | """ 27 | This property can be used as a prefix for any HTTP method call to return 28 | the raw response object instead of the parsed content. 29 | 30 | For more information, see https://www.github.com/RetellAI/retell-python-sdk#accessing-raw-response-data-eg-headers 31 | """ 32 | return VoiceResourceWithRawResponse(self) 33 | 34 | @cached_property 35 | def with_streaming_response(self) -> VoiceResourceWithStreamingResponse: 36 | """ 37 | An alternative to `.with_raw_response` that doesn't eagerly read the response body. 38 | 39 | For more information, see https://www.github.com/RetellAI/retell-python-sdk#with_streaming_response 40 | """ 41 | return VoiceResourceWithStreamingResponse(self) 42 | 43 | def retrieve( 44 | self, 45 | voice_id: str, 46 | *, 47 | # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. 48 | # The extra values given here take precedence over values defined on the client or passed to this method. 49 | extra_headers: Headers | None = None, 50 | extra_query: Query | None = None, 51 | extra_body: Body | None = None, 52 | timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, 53 | ) -> VoiceResponse: 54 | """ 55 | Retrieve details of a specific voice 56 | 57 | Args: 58 | extra_headers: Send extra headers 59 | 60 | extra_query: Add additional query parameters to the request 61 | 62 | extra_body: Add additional JSON properties to the request 63 | 64 | timeout: Override the client-level default timeout for this request, in seconds 65 | """ 66 | if not voice_id: 67 | raise ValueError(f"Expected a non-empty value for `voice_id` but received {voice_id!r}") 68 | return self._get( 69 | f"/get-voice/{voice_id}", 70 | options=make_request_options( 71 | extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout 72 | ), 73 | cast_to=VoiceResponse, 74 | ) 75 | 76 | def list( 77 | self, 78 | *, 79 | # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. 80 | # The extra values given here take precedence over values defined on the client or passed to this method. 81 | extra_headers: Headers | None = None, 82 | extra_query: Query | None = None, 83 | extra_body: Body | None = None, 84 | timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, 85 | ) -> VoiceListResponse: 86 | """List all voices available to the user""" 87 | return self._get( 88 | "/list-voices", 89 | options=make_request_options( 90 | extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout 91 | ), 92 | cast_to=VoiceListResponse, 93 | ) 94 | 95 | 96 | class AsyncVoiceResource(AsyncAPIResource): 97 | @cached_property 98 | def with_raw_response(self) -> AsyncVoiceResourceWithRawResponse: 99 | """ 100 | This property can be used as a prefix for any HTTP method call to return 101 | the raw response object instead of the parsed content. 102 | 103 | For more information, see https://www.github.com/RetellAI/retell-python-sdk#accessing-raw-response-data-eg-headers 104 | """ 105 | return AsyncVoiceResourceWithRawResponse(self) 106 | 107 | @cached_property 108 | def with_streaming_response(self) -> AsyncVoiceResourceWithStreamingResponse: 109 | """ 110 | An alternative to `.with_raw_response` that doesn't eagerly read the response body. 111 | 112 | For more information, see https://www.github.com/RetellAI/retell-python-sdk#with_streaming_response 113 | """ 114 | return AsyncVoiceResourceWithStreamingResponse(self) 115 | 116 | async def retrieve( 117 | self, 118 | voice_id: str, 119 | *, 120 | # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. 121 | # The extra values given here take precedence over values defined on the client or passed to this method. 122 | extra_headers: Headers | None = None, 123 | extra_query: Query | None = None, 124 | extra_body: Body | None = None, 125 | timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, 126 | ) -> VoiceResponse: 127 | """ 128 | Retrieve details of a specific voice 129 | 130 | Args: 131 | extra_headers: Send extra headers 132 | 133 | extra_query: Add additional query parameters to the request 134 | 135 | extra_body: Add additional JSON properties to the request 136 | 137 | timeout: Override the client-level default timeout for this request, in seconds 138 | """ 139 | if not voice_id: 140 | raise ValueError(f"Expected a non-empty value for `voice_id` but received {voice_id!r}") 141 | return await self._get( 142 | f"/get-voice/{voice_id}", 143 | options=make_request_options( 144 | extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout 145 | ), 146 | cast_to=VoiceResponse, 147 | ) 148 | 149 | async def list( 150 | self, 151 | *, 152 | # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. 153 | # The extra values given here take precedence over values defined on the client or passed to this method. 154 | extra_headers: Headers | None = None, 155 | extra_query: Query | None = None, 156 | extra_body: Body | None = None, 157 | timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, 158 | ) -> VoiceListResponse: 159 | """List all voices available to the user""" 160 | return await self._get( 161 | "/list-voices", 162 | options=make_request_options( 163 | extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout 164 | ), 165 | cast_to=VoiceListResponse, 166 | ) 167 | 168 | 169 | class VoiceResourceWithRawResponse: 170 | def __init__(self, voice: VoiceResource) -> None: 171 | self._voice = voice 172 | 173 | self.retrieve = to_raw_response_wrapper( 174 | voice.retrieve, 175 | ) 176 | self.list = to_raw_response_wrapper( 177 | voice.list, 178 | ) 179 | 180 | 181 | class AsyncVoiceResourceWithRawResponse: 182 | def __init__(self, voice: AsyncVoiceResource) -> None: 183 | self._voice = voice 184 | 185 | self.retrieve = async_to_raw_response_wrapper( 186 | voice.retrieve, 187 | ) 188 | self.list = async_to_raw_response_wrapper( 189 | voice.list, 190 | ) 191 | 192 | 193 | class VoiceResourceWithStreamingResponse: 194 | def __init__(self, voice: VoiceResource) -> None: 195 | self._voice = voice 196 | 197 | self.retrieve = to_streamed_response_wrapper( 198 | voice.retrieve, 199 | ) 200 | self.list = to_streamed_response_wrapper( 201 | voice.list, 202 | ) 203 | 204 | 205 | class AsyncVoiceResourceWithStreamingResponse: 206 | def __init__(self, voice: AsyncVoiceResource) -> None: 207 | self._voice = voice 208 | 209 | self.retrieve = async_to_streamed_response_wrapper( 210 | voice.retrieve, 211 | ) 212 | self.list = async_to_streamed_response_wrapper( 213 | voice.list, 214 | ) 215 | -------------------------------------------------------------------------------- /src/retell/types/__init__.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from .llm_response import LlmResponse as LlmResponse 6 | from .call_response import CallResponse as CallResponse 7 | from .chat_response import ChatResponse as ChatResponse 8 | from .agent_response import AgentResponse as AgentResponse 9 | from .voice_response import VoiceResponse as VoiceResponse 10 | from .call_list_params import CallListParams as CallListParams 11 | from .llm_create_params import LlmCreateParams as LlmCreateParams 12 | from .llm_list_response import LlmListResponse as LlmListResponse 13 | from .llm_update_params import LlmUpdateParams as LlmUpdateParams 14 | from .web_call_response import WebCallResponse as WebCallResponse 15 | from .call_list_response import CallListResponse as CallListResponse 16 | from .call_update_params import CallUpdateParams as CallUpdateParams 17 | from .chat_create_params import ChatCreateParams as ChatCreateParams 18 | from .chat_list_response import ChatListResponse as ChatListResponse 19 | from .agent_create_params import AgentCreateParams as AgentCreateParams 20 | from .agent_list_response import AgentListResponse as AgentListResponse 21 | from .agent_update_params import AgentUpdateParams as AgentUpdateParams 22 | from .batch_call_response import BatchCallResponse as BatchCallResponse 23 | from .llm_retrieve_params import LlmRetrieveParams as LlmRetrieveParams 24 | from .phone_call_response import PhoneCallResponse as PhoneCallResponse 25 | from .voice_list_response import VoiceListResponse as VoiceListResponse 26 | from .agent_retrieve_params import AgentRetrieveParams as AgentRetrieveParams 27 | from .phone_number_response import PhoneNumberResponse as PhoneNumberResponse 28 | from .knowledge_base_response import KnowledgeBaseResponse as KnowledgeBaseResponse 29 | from .phone_number_create_params import PhoneNumberCreateParams as PhoneNumberCreateParams 30 | from .phone_number_import_params import PhoneNumberImportParams as PhoneNumberImportParams 31 | from .phone_number_list_response import PhoneNumberListResponse as PhoneNumberListResponse 32 | from .phone_number_update_params import PhoneNumberUpdateParams as PhoneNumberUpdateParams 33 | from .agent_get_versions_response import AgentGetVersionsResponse as AgentGetVersionsResponse 34 | from .call_create_web_call_params import CallCreateWebCallParams as CallCreateWebCallParams 35 | from .knowledge_base_create_params import KnowledgeBaseCreateParams as KnowledgeBaseCreateParams 36 | from .knowledge_base_list_response import KnowledgeBaseListResponse as KnowledgeBaseListResponse 37 | from .call_create_phone_call_params import CallCreatePhoneCallParams as CallCreatePhoneCallParams 38 | from .concurrency_retrieve_response import ConcurrencyRetrieveResponse as ConcurrencyRetrieveResponse 39 | from .call_register_phone_call_params import CallRegisterPhoneCallParams as CallRegisterPhoneCallParams 40 | from .knowledge_base_add_sources_params import KnowledgeBaseAddSourcesParams as KnowledgeBaseAddSourcesParams 41 | from .chat_create_chat_completion_params import ChatCreateChatCompletionParams as ChatCreateChatCompletionParams 42 | from .batch_call_create_batch_call_params import BatchCallCreateBatchCallParams as BatchCallCreateBatchCallParams 43 | from .chat_create_chat_completion_response import ChatCreateChatCompletionResponse as ChatCreateChatCompletionResponse 44 | -------------------------------------------------------------------------------- /src/retell/types/agent_get_versions_response.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from typing import List 4 | from typing_extensions import TypeAlias 5 | 6 | from .agent_response import AgentResponse 7 | 8 | __all__ = ["AgentGetVersionsResponse"] 9 | 10 | AgentGetVersionsResponse: TypeAlias = List[AgentResponse] 11 | -------------------------------------------------------------------------------- /src/retell/types/agent_list_response.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from typing import List 4 | from typing_extensions import TypeAlias 5 | 6 | from .agent_response import AgentResponse 7 | 8 | __all__ = ["AgentListResponse"] 9 | 10 | AgentListResponse: TypeAlias = List[AgentResponse] 11 | -------------------------------------------------------------------------------- /src/retell/types/agent_retrieve_params.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from typing_extensions import TypedDict 6 | 7 | __all__ = ["AgentRetrieveParams"] 8 | 9 | 10 | class AgentRetrieveParams(TypedDict, total=False): 11 | version: int 12 | """Optional version of the API to use for this request. Default to latest version.""" 13 | -------------------------------------------------------------------------------- /src/retell/types/batch_call_create_batch_call_params.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from typing import Dict, Iterable 6 | from typing_extensions import Required, TypedDict 7 | 8 | __all__ = ["BatchCallCreateBatchCallParams", "Task"] 9 | 10 | 11 | class BatchCallCreateBatchCallParams(TypedDict, total=False): 12 | from_number: Required[str] 13 | """The number you own in E.164 format. 14 | 15 | Must be a number purchased from Retell or imported to Retell. 16 | """ 17 | 18 | tasks: Required[Iterable[Task]] 19 | """A list of individual call tasks to be executed as part of the batch call. 20 | 21 | Each task represents a single outbound call and includes details such as the 22 | recipient's phone number and optional dynamic variables to personalize the call 23 | content. 24 | """ 25 | 26 | name: str 27 | """The name of the batch call. Only used for your own reference.""" 28 | 29 | trigger_timestamp: float 30 | """ 31 | The scheduled time for sending the batch call, represented as a Unix timestamp 32 | in milliseconds. If omitted, the call will be sent immediately. 33 | """ 34 | 35 | 36 | class Task(TypedDict, total=False): 37 | to_number: Required[str] 38 | """The The number you want to call, in E.164 format. 39 | 40 | If using a number purchased from Retell, only US numbers are supported as 41 | destination. 42 | """ 43 | 44 | retell_llm_dynamic_variables: Dict[str, object] 45 | """ 46 | Add optional dynamic variables in key value pairs of string that injects into 47 | your Response Engine prompt and tool description. Only applicable for Response 48 | Engine. 49 | """ 50 | -------------------------------------------------------------------------------- /src/retell/types/batch_call_response.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from .._models import BaseModel 4 | 5 | __all__ = ["BatchCallResponse"] 6 | 7 | 8 | class BatchCallResponse(BaseModel): 9 | batch_call_id: str 10 | """Unique id of the batch call.""" 11 | 12 | from_number: str 13 | 14 | name: str 15 | 16 | scheduled_timestamp: float 17 | 18 | total_task_count: float 19 | """Number of tasks within the batch call""" 20 | -------------------------------------------------------------------------------- /src/retell/types/call_create_phone_call_params.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from typing import Dict 6 | from typing_extensions import Required, TypedDict 7 | 8 | __all__ = ["CallCreatePhoneCallParams"] 9 | 10 | 11 | class CallCreatePhoneCallParams(TypedDict, total=False): 12 | from_number: Required[str] 13 | """The number you own in E.164 format. 14 | 15 | Must be a number purchased from Retell or imported to Retell. 16 | """ 17 | 18 | to_number: Required[str] 19 | """The number you want to call, in E.164 format. 20 | 21 | If using a number purchased from Retell, only US numbers are supported as 22 | destination. 23 | """ 24 | 25 | metadata: object 26 | """An arbitrary object for storage purpose only. 27 | 28 | You can put anything here like your internal customer id associated with the 29 | call. Not used for processing. You can later get this field from the call 30 | object. 31 | """ 32 | 33 | override_agent_id: str 34 | """For this particular call, override the agent used with this agent id. 35 | 36 | This does not bind the agent to this number, this is for one time override. 37 | """ 38 | 39 | override_agent_version: int 40 | """For this particular call, override the agent version used with this version. 41 | 42 | This does not bind the agent version to this number, this is for one time 43 | override. 44 | """ 45 | 46 | retell_llm_dynamic_variables: Dict[str, object] 47 | """ 48 | Add optional dynamic variables in key value pairs of string that injects into 49 | your Response Engine prompt and tool description. Only applicable for Response 50 | Engine. 51 | """ 52 | -------------------------------------------------------------------------------- /src/retell/types/call_create_web_call_params.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from typing import Dict 6 | from typing_extensions import Required, TypedDict 7 | 8 | __all__ = ["CallCreateWebCallParams"] 9 | 10 | 11 | class CallCreateWebCallParams(TypedDict, total=False): 12 | agent_id: Required[str] 13 | """Unique id of agent used for the call. 14 | 15 | Your agent would contain the LLM Websocket url used for this call. 16 | """ 17 | 18 | agent_version: int 19 | """The version of the agent to use for the call.""" 20 | 21 | metadata: object 22 | """An arbitrary object for storage purpose only. 23 | 24 | You can put anything here like your internal customer id associated with the 25 | call. Not used for processing. You can later get this field from the call 26 | object. 27 | """ 28 | 29 | retell_llm_dynamic_variables: Dict[str, object] 30 | """ 31 | Add optional dynamic variables in key value pairs of string that injects into 32 | your Response Engine prompt and tool description. Only applicable for Response 33 | Engine. 34 | """ 35 | -------------------------------------------------------------------------------- /src/retell/types/call_list_params.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from typing import List, Iterable 6 | from typing_extensions import Literal, TypedDict 7 | 8 | __all__ = [ 9 | "CallListParams", 10 | "FilterCriteria", 11 | "FilterCriteriaDurationMs", 12 | "FilterCriteriaE2ELatencyP50", 13 | "FilterCriteriaStartTimestamp", 14 | ] 15 | 16 | 17 | class CallListParams(TypedDict, total=False): 18 | filter_criteria: FilterCriteria 19 | """Filter criteria for the calls to retrieve.""" 20 | 21 | limit: int 22 | """Limit the number of calls returned. 23 | 24 | Default 50, Max 1000. To retrieve more than 1000, use pagination_key to continue 25 | fetching the next page. 26 | """ 27 | 28 | pagination_key: str 29 | """The pagination key to continue fetching the next page of calls. 30 | 31 | Pagination key is represented by a call id here, and it's exclusive (not 32 | included in the fetched calls). The last call id from the list calls is usually 33 | used as pagination key here. If not set, will start from the beginning. 34 | """ 35 | 36 | sort_order: Literal["ascending", "descending"] 37 | """ 38 | The calls will be sorted by `start_timestamp`, whether to return the calls in 39 | ascending or descending order. 40 | """ 41 | 42 | 43 | class FilterCriteriaDurationMs(TypedDict, total=False): 44 | lower_threshold: int 45 | 46 | upper_threshold: int 47 | 48 | 49 | class FilterCriteriaE2ELatencyP50(TypedDict, total=False): 50 | lower_threshold: int 51 | 52 | upper_threshold: int 53 | 54 | 55 | class FilterCriteriaStartTimestamp(TypedDict, total=False): 56 | lower_threshold: int 57 | 58 | upper_threshold: int 59 | 60 | 61 | class FilterCriteria(TypedDict, total=False): 62 | agent_id: List[str] 63 | """Only retrieve calls that are made with specific agent(s).""" 64 | 65 | call_status: List[Literal["registered", "ongoing", "ended", "error"]] 66 | """Only retrieve calls with specific call status(es).""" 67 | 68 | call_successful: Iterable[bool] 69 | """Only retrieve calls with specific call successful(s).""" 70 | 71 | call_type: List[Literal["web_call", "phone_call"]] 72 | """Only retrieve calls with specific call type(s).""" 73 | 74 | direction: List[Literal["inbound", "outbound"]] 75 | """Only retrieve calls with specific direction(s).""" 76 | 77 | disconnection_reason: List[ 78 | Literal[ 79 | "user_hangup", 80 | "agent_hangup", 81 | "call_transfer", 82 | "voicemail_reached", 83 | "inactivity", 84 | "machine_detected", 85 | "max_duration_reached", 86 | "concurrency_limit_reached", 87 | "no_valid_payment", 88 | "scam_detected", 89 | "error_inbound_webhook", 90 | "dial_busy", 91 | "dial_failed", 92 | "dial_no_answer", 93 | "error_llm_websocket_open", 94 | "error_llm_websocket_lost_connection", 95 | "error_llm_websocket_runtime", 96 | "error_llm_websocket_corrupt_payload", 97 | "error_no_audio_received", 98 | "error_asr", 99 | "error_retell", 100 | "error_unknown", 101 | "error_user_not_joined", 102 | "registered_call_timeout", 103 | ] 104 | ] 105 | """Only retrieve calls with specific disconnection reason(s).""" 106 | 107 | duration_ms: FilterCriteriaDurationMs 108 | """Only retrieve calls with specific range of duration(s).""" 109 | 110 | e2e_latency_p50: FilterCriteriaE2ELatencyP50 111 | 112 | from_number: List[str] 113 | """Only retrieve calls with specific from number(s).""" 114 | 115 | in_voicemail: Iterable[bool] 116 | """Only retrieve calls that are in voicemail or not in voicemail.""" 117 | 118 | start_timestamp: FilterCriteriaStartTimestamp 119 | """Only retrieve calls with specific range of start timestamp(s).""" 120 | 121 | to_number: List[str] 122 | """Only retrieve calls with specific to number(s).""" 123 | 124 | user_sentiment: List[Literal["Negative", "Positive", "Neutral", "Unknown"]] 125 | """Only retrieve calls with specific user sentiment(s).""" 126 | 127 | version: Iterable[int] 128 | """The version of the agent to use for the call.""" 129 | -------------------------------------------------------------------------------- /src/retell/types/call_list_response.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from typing import List 4 | from typing_extensions import TypeAlias 5 | 6 | from .call_response import CallResponse 7 | 8 | __all__ = ["CallListResponse"] 9 | 10 | CallListResponse: TypeAlias = List[CallResponse] 11 | -------------------------------------------------------------------------------- /src/retell/types/call_register_phone_call_params.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from typing import Dict 6 | from typing_extensions import Literal, Required, TypedDict 7 | 8 | __all__ = ["CallRegisterPhoneCallParams"] 9 | 10 | 11 | class CallRegisterPhoneCallParams(TypedDict, total=False): 12 | agent_id: Required[str] 13 | """The agent to use for the call.""" 14 | 15 | agent_version: int 16 | """The version of the agent to use for the call.""" 17 | 18 | direction: Literal["inbound", "outbound"] 19 | """Direction of the phone call. Stored for tracking purpose.""" 20 | 21 | from_number: str 22 | """The number you own in E.164 format. Stored for tracking purpose.""" 23 | 24 | metadata: object 25 | """An arbitrary object for storage purpose only. 26 | 27 | You can put anything here like your internal customer id associated with the 28 | call. Not used for processing. You can later get this field from the call 29 | object. 30 | """ 31 | 32 | retell_llm_dynamic_variables: Dict[str, object] 33 | """ 34 | Add optional dynamic variables in key value pairs of string that injects into 35 | your Response Engine prompt and tool description. Only applicable for Response 36 | Engine. 37 | """ 38 | 39 | to_number: str 40 | """The number you want to call, in E.164 format. Stored for tracking purpose.""" 41 | -------------------------------------------------------------------------------- /src/retell/types/call_response.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from typing import Union 4 | from typing_extensions import TypeAlias 5 | 6 | from .web_call_response import WebCallResponse 7 | from .phone_call_response import PhoneCallResponse 8 | 9 | __all__ = ["CallResponse"] 10 | 11 | CallResponse: TypeAlias = Union[WebCallResponse, PhoneCallResponse] 12 | -------------------------------------------------------------------------------- /src/retell/types/call_update_params.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from typing_extensions import TypedDict 6 | 7 | __all__ = ["CallUpdateParams"] 8 | 9 | 10 | class CallUpdateParams(TypedDict, total=False): 11 | metadata: object 12 | """An arbitrary object for storage purpose only. 13 | 14 | You can put anything here like your internal customer id associated with the 15 | call. Not used for processing. You can later get this field from the call 16 | object. Size limited to 50kB max. 17 | """ 18 | 19 | opt_out_sensitive_data_storage: bool 20 | """ 21 | Whether this call opts out of sensitive data storage like transcript, recording, 22 | logging. Can only be changed from false to true. 23 | """ 24 | -------------------------------------------------------------------------------- /src/retell/types/chat_create_chat_completion_params.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from typing_extensions import Required, TypedDict 6 | 7 | __all__ = ["ChatCreateChatCompletionParams"] 8 | 9 | 10 | class ChatCreateChatCompletionParams(TypedDict, total=False): 11 | chat_id: Required[str] 12 | """Unique id of the chat to create completion.""" 13 | 14 | content: Required[str] 15 | """user message to generate agent chat completion.""" 16 | -------------------------------------------------------------------------------- /src/retell/types/chat_create_chat_completion_response.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from typing import List, Union, Optional 4 | from typing_extensions import Literal, TypeAlias 5 | 6 | from .._models import BaseModel 7 | 8 | __all__ = [ 9 | "ChatCreateChatCompletionResponse", 10 | "Message", 11 | "MessageMessage", 12 | "MessageToolCallInvocationMessage", 13 | "MessageToolCallResultMessage", 14 | "MessageNodeTransitionMessage", 15 | "MessageStateTransitionMessage", 16 | ] 17 | 18 | 19 | class MessageMessage(BaseModel): 20 | content: str 21 | """Content of the message""" 22 | 23 | created_timestamp: int 24 | """Create timestamp of the message""" 25 | 26 | message_id: str 27 | """Unique id ot the message""" 28 | 29 | role: Literal["agent", "user"] 30 | """Documents whether this message is sent by agent or user.""" 31 | 32 | 33 | class MessageToolCallInvocationMessage(BaseModel): 34 | arguments: str 35 | """Arguments for this tool call, it's a stringified JSON object.""" 36 | 37 | message_id: str 38 | """Unique id ot the message""" 39 | 40 | name: str 41 | """Name of the function in this tool call.""" 42 | 43 | role: Literal["tool_call_invocation"] 44 | """This is a tool call invocation.""" 45 | 46 | tool_call_id: str 47 | """Tool call id, globally unique.""" 48 | 49 | created_timestamp: Optional[int] = None 50 | """Create timestamp of the message""" 51 | 52 | 53 | class MessageToolCallResultMessage(BaseModel): 54 | content: str 55 | """Result of the tool call, can be a string, a stringified json, etc.""" 56 | 57 | created_timestamp: int 58 | """Create timestamp of the message""" 59 | 60 | message_id: str 61 | """Unique id ot the message""" 62 | 63 | role: Literal["tool_call_result"] 64 | """This is result of a tool call.""" 65 | 66 | tool_call_id: str 67 | """Tool call id, globally unique.""" 68 | 69 | 70 | class MessageNodeTransitionMessage(BaseModel): 71 | created_timestamp: int 72 | """Create timestamp of the message""" 73 | 74 | message_id: str 75 | """Unique id ot the message""" 76 | 77 | role: Literal["node_transition"] 78 | """This is node transition.""" 79 | 80 | former_node_id: Optional[str] = None 81 | """Former node id""" 82 | 83 | former_node_name: Optional[str] = None 84 | """Former node name""" 85 | 86 | new_node_id: Optional[str] = None 87 | """New node id""" 88 | 89 | new_node_name: Optional[str] = None 90 | """New node name""" 91 | 92 | 93 | class MessageStateTransitionMessage(BaseModel): 94 | created_timestamp: int 95 | """Create timestamp of the message""" 96 | 97 | message_id: str 98 | """Unique id ot the message""" 99 | 100 | role: Literal["state_transition"] 101 | """This is state transition for .""" 102 | 103 | former_state_name: Optional[str] = None 104 | """Former state name""" 105 | 106 | new_state_name: Optional[str] = None 107 | """New state name""" 108 | 109 | 110 | Message: TypeAlias = Union[ 111 | MessageMessage, 112 | MessageToolCallInvocationMessage, 113 | MessageToolCallResultMessage, 114 | MessageNodeTransitionMessage, 115 | MessageStateTransitionMessage, 116 | ] 117 | 118 | 119 | class ChatCreateChatCompletionResponse(BaseModel): 120 | messages: List[Message] 121 | """ 122 | New messages generated by the agent during this completion, including any tool 123 | call invocations and their results. Does not include the original input 124 | messages. 125 | """ 126 | -------------------------------------------------------------------------------- /src/retell/types/chat_create_params.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from typing import Dict 6 | from typing_extensions import Required, TypedDict 7 | 8 | __all__ = ["ChatCreateParams"] 9 | 10 | 11 | class ChatCreateParams(TypedDict, total=False): 12 | agent_id: Required[str] 13 | """The chat agent to use for the call.""" 14 | 15 | agent_version: int 16 | """The version of the chat agent to use for the call.""" 17 | 18 | metadata: object 19 | """An arbitrary object for storage purpose only. 20 | 21 | You can put anything here like your internal customer id associated with the 22 | chat. Not used for processing. You can later get this field from the chat 23 | object. 24 | """ 25 | 26 | retell_llm_dynamic_variables: Dict[str, object] 27 | """ 28 | Add optional dynamic variables in key value pairs of string that injects into 29 | your Response Engine prompt and tool description. Only applicable for Response 30 | Engine. 31 | """ 32 | -------------------------------------------------------------------------------- /src/retell/types/chat_list_response.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from typing import List 4 | from typing_extensions import TypeAlias 5 | 6 | from .chat_response import ChatResponse 7 | 8 | __all__ = ["ChatListResponse"] 9 | 10 | ChatListResponse: TypeAlias = List[ChatResponse] 11 | -------------------------------------------------------------------------------- /src/retell/types/chat_response.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from typing import Dict, List, Union, Optional 4 | from typing_extensions import Literal, TypeAlias 5 | 6 | from pydantic import Field as FieldInfo 7 | 8 | from .._models import BaseModel 9 | 10 | __all__ = [ 11 | "ChatResponse", 12 | "ChatAnalysis", 13 | "ChatCost", 14 | "ChatCostProductCost", 15 | "MessageWithToolCall", 16 | "MessageWithToolCallMessage", 17 | "MessageWithToolCallToolCallInvocationMessage", 18 | "MessageWithToolCallToolCallResultMessage", 19 | "MessageWithToolCallNodeTransitionMessage", 20 | "MessageWithToolCallStateTransitionMessage", 21 | ] 22 | 23 | 24 | class ChatAnalysis(BaseModel): 25 | chat_successful: Optional[bool] = None 26 | """ 27 | Whether the agent seems to have a successful chat with the user, where the agent 28 | finishes the task, and the call was complete without being cutoff. 29 | """ 30 | 31 | chat_summary: Optional[str] = None 32 | """A high level summary of the chat.""" 33 | 34 | custom_analysis_data: Optional[object] = None 35 | """ 36 | Custom analysis data that was extracted based on the schema defined in chat 37 | agent post chat analysis data. Can be empty if nothing is specified. 38 | """ 39 | 40 | user_sentiment: Optional[Literal["Negative", "Positive", "Neutral", "Unknown"]] = None 41 | """Sentiment of the user in the chat.""" 42 | 43 | 44 | class ChatCostProductCost(BaseModel): 45 | cost: float 46 | """Cost for the product in cents for the duration of the call.""" 47 | 48 | product: str 49 | """Product name that has a cost associated with it.""" 50 | 51 | unit_price: float = FieldInfo(alias="unitPrice") 52 | """Unit price of the product in cents per second.""" 53 | 54 | 55 | class ChatCost(BaseModel): 56 | combined_cost: Optional[float] = None 57 | """Combined cost of all individual costs in cents""" 58 | 59 | product_costs: Optional[List[ChatCostProductCost]] = None 60 | """List of products with their unit prices and costs in cents""" 61 | 62 | 63 | class MessageWithToolCallMessage(BaseModel): 64 | content: str 65 | """Content of the message""" 66 | 67 | created_timestamp: int 68 | """Create timestamp of the message""" 69 | 70 | message_id: str 71 | """Unique id ot the message""" 72 | 73 | role: Literal["agent", "user"] 74 | """Documents whether this message is sent by agent or user.""" 75 | 76 | 77 | class MessageWithToolCallToolCallInvocationMessage(BaseModel): 78 | arguments: str 79 | """Arguments for this tool call, it's a stringified JSON object.""" 80 | 81 | message_id: str 82 | """Unique id ot the message""" 83 | 84 | name: str 85 | """Name of the function in this tool call.""" 86 | 87 | role: Literal["tool_call_invocation"] 88 | """This is a tool call invocation.""" 89 | 90 | tool_call_id: str 91 | """Tool call id, globally unique.""" 92 | 93 | created_timestamp: Optional[int] = None 94 | """Create timestamp of the message""" 95 | 96 | 97 | class MessageWithToolCallToolCallResultMessage(BaseModel): 98 | content: str 99 | """Result of the tool call, can be a string, a stringified json, etc.""" 100 | 101 | created_timestamp: int 102 | """Create timestamp of the message""" 103 | 104 | message_id: str 105 | """Unique id ot the message""" 106 | 107 | role: Literal["tool_call_result"] 108 | """This is result of a tool call.""" 109 | 110 | tool_call_id: str 111 | """Tool call id, globally unique.""" 112 | 113 | 114 | class MessageWithToolCallNodeTransitionMessage(BaseModel): 115 | created_timestamp: int 116 | """Create timestamp of the message""" 117 | 118 | message_id: str 119 | """Unique id ot the message""" 120 | 121 | role: Literal["node_transition"] 122 | """This is node transition.""" 123 | 124 | former_node_id: Optional[str] = None 125 | """Former node id""" 126 | 127 | former_node_name: Optional[str] = None 128 | """Former node name""" 129 | 130 | new_node_id: Optional[str] = None 131 | """New node id""" 132 | 133 | new_node_name: Optional[str] = None 134 | """New node name""" 135 | 136 | 137 | class MessageWithToolCallStateTransitionMessage(BaseModel): 138 | created_timestamp: int 139 | """Create timestamp of the message""" 140 | 141 | message_id: str 142 | """Unique id ot the message""" 143 | 144 | role: Literal["state_transition"] 145 | """This is state transition for .""" 146 | 147 | former_state_name: Optional[str] = None 148 | """Former state name""" 149 | 150 | new_state_name: Optional[str] = None 151 | """New state name""" 152 | 153 | 154 | MessageWithToolCall: TypeAlias = Union[ 155 | MessageWithToolCallMessage, 156 | MessageWithToolCallToolCallInvocationMessage, 157 | MessageWithToolCallToolCallResultMessage, 158 | MessageWithToolCallNodeTransitionMessage, 159 | MessageWithToolCallStateTransitionMessage, 160 | ] 161 | 162 | 163 | class ChatResponse(BaseModel): 164 | agent_id: str 165 | """Corresponding chat agent id of this chat.""" 166 | 167 | chat_id: str 168 | """Unique id of the chat.""" 169 | 170 | chat_status: Literal["ongoing", "ended", "error"] 171 | """Status of chat. 172 | 173 | - `ongoing`: Chat session is ongoing, chat agent can receive new message and 174 | generate response. 175 | 176 | - `ended`: Chat session has ended can not generate new response. 177 | 178 | - `error`: Chat encountered error. 179 | """ 180 | 181 | chat_analysis: Optional[ChatAnalysis] = None 182 | """ 183 | Post chat analysis that includes information such as sentiment, status, summary, 184 | and custom defined data to extract. Available after chat ends. Subscribe to 185 | `chat_analyzed` webhook event type to receive it once ready. 186 | """ 187 | 188 | chat_cost: Optional[ChatCost] = None 189 | 190 | collected_dynamic_variables: Optional[Dict[str, object]] = None 191 | """Dynamic variables collected from the chat. Only available after the chat ends.""" 192 | 193 | end_timestamp: Optional[int] = None 194 | """End timestamp (milliseconds since epoch) of the chat. 195 | 196 | Available after chat ends. 197 | """ 198 | 199 | message_with_tool_calls: Optional[List[MessageWithToolCall]] = None 200 | """Transcript of the chat weaved with tool call invocation and results.""" 201 | 202 | metadata: Optional[object] = None 203 | """An arbitrary object for storage purpose only. 204 | 205 | You can put anything here like your internal customer id associated with the 206 | chat. Not used for processing. You can later get this field from the chat 207 | object. 208 | """ 209 | 210 | retell_llm_dynamic_variables: Optional[Dict[str, object]] = None 211 | """ 212 | Add optional dynamic variables in key value pairs of string that injects into 213 | your Response Engine prompt and tool description. Only applicable for Response 214 | Engine. 215 | """ 216 | 217 | start_timestamp: Optional[int] = None 218 | """Begin timestamp (milliseconds since epoch) of the chat. 219 | 220 | Available after chat starts. 221 | """ 222 | 223 | transcript: Optional[str] = None 224 | """Transcription of the chat.""" 225 | -------------------------------------------------------------------------------- /src/retell/types/concurrency_retrieve_response.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from typing import Optional 4 | 5 | from .._models import BaseModel 6 | 7 | __all__ = ["ConcurrencyRetrieveResponse"] 8 | 9 | 10 | class ConcurrencyRetrieveResponse(BaseModel): 11 | base_concurrency: Optional[int] = None 12 | """The free concurrency limit of the org.""" 13 | 14 | concurrency_limit: Optional[int] = None 15 | """ 16 | The total concurrency limit (at max how many ongoing calls one can make) of the 17 | org. This should be the sum of `base_concurrency` and `purchased_concurrency`. 18 | """ 19 | 20 | concurrency_purchase_limit: Optional[int] = None 21 | """The maximum amount of concurrency that the org can purchase.""" 22 | 23 | current_concurrency: Optional[int] = None 24 | """The current concurrency (amount of ongoing calls) of the org.""" 25 | 26 | purchased_concurrency: Optional[int] = None 27 | """The amount of concurrency that the org has already purchased.""" 28 | 29 | remaining_purchase_limit: Optional[int] = None 30 | """The remaining amount of concurrency that the org can purchase. 31 | 32 | This is the difference between `concurrency_purchase_limit` and 33 | `purchased_concurrency`. 34 | """ 35 | -------------------------------------------------------------------------------- /src/retell/types/knowledge_base_add_sources_params.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from typing import List, Iterable 6 | from typing_extensions import Required, TypedDict 7 | 8 | from .._types import FileTypes 9 | 10 | __all__ = ["KnowledgeBaseAddSourcesParams", "KnowledgeBaseText"] 11 | 12 | 13 | class KnowledgeBaseAddSourcesParams(TypedDict, total=False): 14 | knowledge_base_files: List[FileTypes] 15 | """Files to add to the knowledge base. 16 | 17 | Limit to 25 files, where each file is limited to 50MB. 18 | """ 19 | 20 | knowledge_base_texts: Iterable[KnowledgeBaseText] 21 | """Texts to add to the knowledge base.""" 22 | 23 | knowledge_base_urls: List[str] 24 | """URLs to be scraped and added to the knowledge base. Must be valid urls.""" 25 | 26 | 27 | class KnowledgeBaseText(TypedDict, total=False): 28 | text: Required[str] 29 | """Text to add to the knowledge base.""" 30 | 31 | title: Required[str] 32 | """Title of the text.""" 33 | -------------------------------------------------------------------------------- /src/retell/types/knowledge_base_create_params.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from typing import List, Iterable 6 | from typing_extensions import Required, TypedDict 7 | 8 | from .._types import FileTypes 9 | 10 | __all__ = ["KnowledgeBaseCreateParams", "KnowledgeBaseText"] 11 | 12 | 13 | class KnowledgeBaseCreateParams(TypedDict, total=False): 14 | knowledge_base_name: Required[str] 15 | """Name of the knowledge base. Must be less than 40 characters.""" 16 | 17 | enable_auto_refresh: bool 18 | """Whether to enable auto refresh for the knowledge base urls. 19 | 20 | If set to true, will retrieve the data from the specified url every 12 hours. 21 | """ 22 | 23 | knowledge_base_files: List[FileTypes] 24 | """Files to add to the knowledge base. 25 | 26 | Limit to 25 files, where each file is limited to 50MB. 27 | """ 28 | 29 | knowledge_base_texts: Iterable[KnowledgeBaseText] 30 | """Texts to add to the knowledge base.""" 31 | 32 | knowledge_base_urls: List[str] 33 | """URLs to be scraped and added to the knowledge base. Must be valid urls.""" 34 | 35 | 36 | class KnowledgeBaseText(TypedDict, total=False): 37 | text: Required[str] 38 | """Text to add to the knowledge base.""" 39 | 40 | title: Required[str] 41 | """Title of the text.""" 42 | -------------------------------------------------------------------------------- /src/retell/types/knowledge_base_list_response.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from typing import List 4 | from typing_extensions import TypeAlias 5 | 6 | from .knowledge_base_response import KnowledgeBaseResponse 7 | 8 | __all__ = ["KnowledgeBaseListResponse"] 9 | 10 | KnowledgeBaseListResponse: TypeAlias = List[KnowledgeBaseResponse] 11 | -------------------------------------------------------------------------------- /src/retell/types/knowledge_base_response.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from typing import List, Union, Optional 4 | from typing_extensions import Literal, TypeAlias 5 | 6 | from .._models import BaseModel 7 | 8 | __all__ = [ 9 | "KnowledgeBaseResponse", 10 | "KnowledgeBaseSource", 11 | "KnowledgeBaseSourceKnowledgeBaseSourceDocument", 12 | "KnowledgeBaseSourceKnowledgeBaseSourceText", 13 | "KnowledgeBaseSourceKnowledgeBaseSourceURL", 14 | ] 15 | 16 | 17 | class KnowledgeBaseSourceKnowledgeBaseSourceDocument(BaseModel): 18 | file_url: str 19 | """URL of the document stored.""" 20 | 21 | filename: str 22 | """Filename of the document.""" 23 | 24 | source_id: str 25 | """Unique id of the knowledge base source.""" 26 | 27 | type: Literal["document"] 28 | """Type of the knowledge base source.""" 29 | 30 | 31 | class KnowledgeBaseSourceKnowledgeBaseSourceText(BaseModel): 32 | content_url: str 33 | """URL of the text content stored.""" 34 | 35 | source_id: str 36 | """Unique id of the knowledge base source.""" 37 | 38 | title: str 39 | """Title of the text.""" 40 | 41 | type: Literal["text"] 42 | """Type of the knowledge base source.""" 43 | 44 | 45 | class KnowledgeBaseSourceKnowledgeBaseSourceURL(BaseModel): 46 | source_id: str 47 | """Unique id of the knowledge base source.""" 48 | 49 | type: Literal["url"] 50 | """Type of the knowledge base source.""" 51 | 52 | url: str 53 | """URL used to be scraped and added to the knowledge base.""" 54 | 55 | 56 | KnowledgeBaseSource: TypeAlias = Union[ 57 | KnowledgeBaseSourceKnowledgeBaseSourceDocument, 58 | KnowledgeBaseSourceKnowledgeBaseSourceText, 59 | KnowledgeBaseSourceKnowledgeBaseSourceURL, 60 | ] 61 | 62 | 63 | class KnowledgeBaseResponse(BaseModel): 64 | knowledge_base_id: str 65 | """Unique id of the knowledge base.""" 66 | 67 | knowledge_base_name: str 68 | """Name of the knowledge base. Must be less than 40 characters.""" 69 | 70 | status: Literal["in_progress", "complete", "error"] 71 | """Status of the knowledge base. 72 | 73 | When it's created and being processed, it's "in_progress". When the processing 74 | is done, it's "complete". When there's an error in processing, it's "error". 75 | """ 76 | 77 | enable_auto_refresh: Optional[bool] = None 78 | """Whether to enable auto refresh for the knowledge base urls. 79 | 80 | If set to true, will retrieve the data from the specified url every 12 hours. 81 | """ 82 | 83 | knowledge_base_sources: Optional[List[KnowledgeBaseSource]] = None 84 | """Sources of the knowledge base. 85 | 86 | Will be populated after the processing is done (when status is "complete"). 87 | """ 88 | 89 | last_refreshed_timestamp: Optional[int] = None 90 | """Last refreshed timestamp (milliseconds since epoch). 91 | 92 | Only applicable when enable_auto_refresh is true. 93 | """ 94 | -------------------------------------------------------------------------------- /src/retell/types/llm_list_response.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from typing import List 4 | from typing_extensions import TypeAlias 5 | 6 | from .llm_response import LlmResponse 7 | 8 | __all__ = ["LlmListResponse"] 9 | 10 | LlmListResponse: TypeAlias = List[LlmResponse] 11 | -------------------------------------------------------------------------------- /src/retell/types/llm_retrieve_params.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from typing_extensions import TypedDict 6 | 7 | __all__ = ["LlmRetrieveParams"] 8 | 9 | 10 | class LlmRetrieveParams(TypedDict, total=False): 11 | version: int 12 | """Optional version of the API to use for this request. Default to latest version.""" 13 | -------------------------------------------------------------------------------- /src/retell/types/phone_number_create_params.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from typing import Optional 6 | from typing_extensions import Literal, TypedDict 7 | 8 | __all__ = ["PhoneNumberCreateParams"] 9 | 10 | 11 | class PhoneNumberCreateParams(TypedDict, total=False): 12 | area_code: int 13 | """Area code of the number to obtain. 14 | 15 | Format is a 3 digit integer. Currently only supports US area code. 16 | """ 17 | 18 | inbound_agent_id: Optional[str] 19 | """Unique id of agent to bind to the number. 20 | 21 | The number will automatically use the agent when receiving inbound calls. If 22 | null, this number would not accept inbound call. 23 | """ 24 | 25 | inbound_agent_version: Optional[int] 26 | """Version of the inbound agent to bind to the number. 27 | 28 | If not provided, will default to latest version. 29 | """ 30 | 31 | inbound_webhook_url: Optional[str] 32 | """ 33 | If set, will send a webhook for inbound calls, where you can to override agent 34 | id, set dynamic variables and other fields specific to that call. 35 | """ 36 | 37 | nickname: str 38 | """Nickname of the number. This is for your reference only.""" 39 | 40 | number_provider: Literal["twilio", "telnyx"] 41 | """The provider to purchase the phone number from. Default to twilio.""" 42 | 43 | outbound_agent_id: Optional[str] 44 | """Unique id of agent to bind to the number. 45 | 46 | The number will automatically use the agent when conducting outbound calls. If 47 | null, this number would not be able to initiate outbound call without agent id 48 | override. 49 | """ 50 | 51 | outbound_agent_version: Optional[int] 52 | """Version of the outbound agent to bind to the number. 53 | 54 | If not provided, will default to latest version. 55 | """ 56 | -------------------------------------------------------------------------------- /src/retell/types/phone_number_import_params.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from typing import Optional 6 | from typing_extensions import Required, TypedDict 7 | 8 | __all__ = ["PhoneNumberImportParams"] 9 | 10 | 11 | class PhoneNumberImportParams(TypedDict, total=False): 12 | phone_number: Required[str] 13 | """ 14 | The number you are trying to import in E.164 format of the number (+country 15 | code, then number with no space, no special characters), used as the unique 16 | identifier for phone number APIs. 17 | """ 18 | 19 | termination_uri: Required[str] 20 | """The termination uri to uniquely identify your elastic SIP trunk. 21 | 22 | This is used for outbound calls. For Twilio elastic SIP trunks it always end 23 | with ".pstn.twilio.com". 24 | """ 25 | 26 | inbound_agent_id: Optional[str] 27 | """Unique id of agent to bind to the number. 28 | 29 | The number will automatically use the agent when receiving inbound calls. If 30 | null, this number would not accept inbound call. 31 | """ 32 | 33 | inbound_agent_version: Optional[int] 34 | """Version of the inbound agent to bind to the number. 35 | 36 | If not provided, will default to latest version. 37 | """ 38 | 39 | inbound_webhook_url: Optional[str] 40 | """ 41 | If set, will send a webhook for inbound calls, where you can to override agent 42 | id, set dynamic variables and other fields specific to that call. 43 | """ 44 | 45 | nickname: str 46 | """Nickname of the number. This is for your reference only.""" 47 | 48 | outbound_agent_id: str 49 | """Unique id of agent to bind to the number. 50 | 51 | The number will automatically use the agent when conducting outbound calls. If 52 | null, this number would not be able to initiate outbound call without agent id 53 | override. 54 | """ 55 | 56 | outbound_agent_version: Optional[int] 57 | """Version of the outbound agent to bind to the number. 58 | 59 | If not provided, will default to latest version. 60 | """ 61 | 62 | sip_trunk_auth_password: str 63 | """The password used for authentication for the SIP trunk.""" 64 | 65 | sip_trunk_auth_username: str 66 | """The username used for authentication for the SIP trunk.""" 67 | -------------------------------------------------------------------------------- /src/retell/types/phone_number_list_response.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from typing import List 4 | from typing_extensions import TypeAlias 5 | 6 | from .phone_number_response import PhoneNumberResponse 7 | 8 | __all__ = ["PhoneNumberListResponse"] 9 | 10 | PhoneNumberListResponse: TypeAlias = List[PhoneNumberResponse] 11 | -------------------------------------------------------------------------------- /src/retell/types/phone_number_response.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from typing import Optional 4 | from typing_extensions import Literal 5 | 6 | from .._models import BaseModel 7 | 8 | __all__ = ["PhoneNumberResponse"] 9 | 10 | 11 | class PhoneNumberResponse(BaseModel): 12 | last_modification_timestamp: int 13 | """Last modification timestamp (milliseconds since epoch). 14 | 15 | Either the time of last update or creation if no updates available. 16 | """ 17 | 18 | phone_number: str 19 | """ 20 | E.164 format of the number (+country code, then number with no space, no special 21 | characters), used as the unique identifier for phone number APIs. 22 | """ 23 | 24 | area_code: Optional[int] = None 25 | """Area code of the number to obtain. 26 | 27 | Format is a 3 digit integer. Currently only supports US area code. 28 | """ 29 | 30 | inbound_agent_id: Optional[str] = None 31 | """Unique id of agent to bind to the number. 32 | 33 | The number will automatically use the agent when receiving inbound calls. If 34 | null, this number would not accept inbound call. 35 | """ 36 | 37 | inbound_agent_version: Optional[int] = None 38 | """Version of the inbound agent to bind to the number. 39 | 40 | If not provided, will default to latest version. 41 | """ 42 | 43 | inbound_webhook_url: Optional[str] = None 44 | """ 45 | If set, will send a webhook for inbound calls, where you can to override agent 46 | id, set dynamic variables and other fields specific to that call. 47 | """ 48 | 49 | nickname: Optional[str] = None 50 | """Nickname of the number. This is for your reference only.""" 51 | 52 | outbound_agent_id: Optional[str] = None 53 | """Unique id of agent to bind to the number. 54 | 55 | The number will automatically use the agent when conducting outbound calls. If 56 | null, this number would not be able to initiate outbound call without agent id 57 | override. 58 | """ 59 | 60 | outbound_agent_version: Optional[int] = None 61 | """Version of the outbound agent to bind to the number. 62 | 63 | If not provided, will default to latest version. 64 | """ 65 | 66 | phone_number_pretty: Optional[str] = None 67 | """Pretty printed phone number, provided for your reference.""" 68 | 69 | phone_number_type: Optional[Literal["retell-twilio", "retell-telnyx", "custom"]] = None 70 | """Type of the phone number.""" 71 | -------------------------------------------------------------------------------- /src/retell/types/phone_number_update_params.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | from typing import Optional 6 | from typing_extensions import TypedDict 7 | 8 | __all__ = ["PhoneNumberUpdateParams"] 9 | 10 | 11 | class PhoneNumberUpdateParams(TypedDict, total=False): 12 | inbound_agent_id: Optional[str] 13 | """Unique id of agent to bind to the number. 14 | 15 | The number will automatically use the agent when receiving inbound calls. If set 16 | to null, this number would not accept inbound call. 17 | """ 18 | 19 | inbound_agent_version: Optional[int] 20 | """Version of the inbound agent to bind to the number. 21 | 22 | If not provided, will default to latest version. 23 | """ 24 | 25 | inbound_webhook_url: Optional[str] 26 | """ 27 | If set, will send a webhook for inbound calls, where you can to override agent 28 | id, set dynamic variables and other fields specific to that call. 29 | """ 30 | 31 | nickname: Optional[str] 32 | """Nickname of the number. This is for your reference only.""" 33 | 34 | outbound_agent_id: Optional[str] 35 | """Unique id of agent to bind to the number. 36 | 37 | The number will automatically use the agent when conducting outbound calls. If 38 | set to null, this number would not be able to initiate outbound call without 39 | agent id override. 40 | """ 41 | 42 | outbound_agent_version: Optional[int] 43 | """Version of the outbound agent to bind to the number. 44 | 45 | If not provided, will default to latest version. 46 | """ 47 | -------------------------------------------------------------------------------- /src/retell/types/voice_list_response.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from typing import List 4 | from typing_extensions import TypeAlias 5 | 6 | from .voice_response import VoiceResponse 7 | 8 | __all__ = ["VoiceListResponse"] 9 | 10 | VoiceListResponse: TypeAlias = List[VoiceResponse] 11 | -------------------------------------------------------------------------------- /src/retell/types/voice_response.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from typing import Optional 4 | from typing_extensions import Literal 5 | 6 | from .._models import BaseModel 7 | 8 | __all__ = ["VoiceResponse"] 9 | 10 | 11 | class VoiceResponse(BaseModel): 12 | gender: Literal["male", "female"] 13 | """Gender of voice.""" 14 | 15 | provider: Literal["elevenlabs", "openai", "deepgram"] 16 | """Indicates the provider of voice.""" 17 | 18 | voice_id: str 19 | """Unique id for the voice.""" 20 | 21 | voice_name: str 22 | """Name of the voice.""" 23 | 24 | accent: Optional[str] = None 25 | """Accent annotation of the voice.""" 26 | 27 | age: Optional[str] = None 28 | """Age annotation of the voice.""" 29 | 30 | preview_audio_url: Optional[str] = None 31 | """URL to the preview audio of the voice.""" 32 | -------------------------------------------------------------------------------- /src/retell_ai/lib/.keep: -------------------------------------------------------------------------------- 1 | File generated from our OpenAPI spec by Stainless. 2 | 3 | This directory can be used to store custom files to expand the SDK. 4 | It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. -------------------------------------------------------------------------------- /src/retell_sdk/lib/.keep: -------------------------------------------------------------------------------- 1 | File generated from our OpenAPI spec by Stainless. 2 | 3 | This directory can be used to store custom files to expand the SDK. 4 | It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. -------------------------------------------------------------------------------- /src/toddlzt/lib/.keep: -------------------------------------------------------------------------------- 1 | File generated from our OpenAPI spec by Stainless. 2 | 3 | This directory can be used to store custom files to expand the SDK. 4 | It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | -------------------------------------------------------------------------------- /tests/api_resources/__init__.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | -------------------------------------------------------------------------------- /tests/api_resources/test_batch_call.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | import os 6 | from typing import Any, cast 7 | 8 | import pytest 9 | 10 | from retell import Retell, AsyncRetell 11 | from tests.utils import assert_matches_type 12 | from retell.types import BatchCallResponse 13 | 14 | base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") 15 | 16 | 17 | class TestBatchCall: 18 | parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) 19 | 20 | @parametrize 21 | def test_method_create_batch_call(self, client: Retell) -> None: 22 | batch_call = client.batch_call.create_batch_call( 23 | from_number="+14157774444", 24 | tasks=[{"to_number": "+12137774445"}], 25 | ) 26 | assert_matches_type(BatchCallResponse, batch_call, path=["response"]) 27 | 28 | @parametrize 29 | def test_method_create_batch_call_with_all_params(self, client: Retell) -> None: 30 | batch_call = client.batch_call.create_batch_call( 31 | from_number="+14157774444", 32 | tasks=[ 33 | { 34 | "to_number": "+12137774445", 35 | "retell_llm_dynamic_variables": {"customer_name": "bar"}, 36 | } 37 | ], 38 | name="First batch call", 39 | trigger_timestamp=1735718400000, 40 | ) 41 | assert_matches_type(BatchCallResponse, batch_call, path=["response"]) 42 | 43 | @parametrize 44 | def test_raw_response_create_batch_call(self, client: Retell) -> None: 45 | response = client.batch_call.with_raw_response.create_batch_call( 46 | from_number="+14157774444", 47 | tasks=[{"to_number": "+12137774445"}], 48 | ) 49 | 50 | assert response.is_closed is True 51 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 52 | batch_call = response.parse() 53 | assert_matches_type(BatchCallResponse, batch_call, path=["response"]) 54 | 55 | @parametrize 56 | def test_streaming_response_create_batch_call(self, client: Retell) -> None: 57 | with client.batch_call.with_streaming_response.create_batch_call( 58 | from_number="+14157774444", 59 | tasks=[{"to_number": "+12137774445"}], 60 | ) as response: 61 | assert not response.is_closed 62 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 63 | 64 | batch_call = response.parse() 65 | assert_matches_type(BatchCallResponse, batch_call, path=["response"]) 66 | 67 | assert cast(Any, response.is_closed) is True 68 | 69 | 70 | class TestAsyncBatchCall: 71 | parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) 72 | 73 | @parametrize 74 | async def test_method_create_batch_call(self, async_client: AsyncRetell) -> None: 75 | batch_call = await async_client.batch_call.create_batch_call( 76 | from_number="+14157774444", 77 | tasks=[{"to_number": "+12137774445"}], 78 | ) 79 | assert_matches_type(BatchCallResponse, batch_call, path=["response"]) 80 | 81 | @parametrize 82 | async def test_method_create_batch_call_with_all_params(self, async_client: AsyncRetell) -> None: 83 | batch_call = await async_client.batch_call.create_batch_call( 84 | from_number="+14157774444", 85 | tasks=[ 86 | { 87 | "to_number": "+12137774445", 88 | "retell_llm_dynamic_variables": {"customer_name": "bar"}, 89 | } 90 | ], 91 | name="First batch call", 92 | trigger_timestamp=1735718400000, 93 | ) 94 | assert_matches_type(BatchCallResponse, batch_call, path=["response"]) 95 | 96 | @parametrize 97 | async def test_raw_response_create_batch_call(self, async_client: AsyncRetell) -> None: 98 | response = await async_client.batch_call.with_raw_response.create_batch_call( 99 | from_number="+14157774444", 100 | tasks=[{"to_number": "+12137774445"}], 101 | ) 102 | 103 | assert response.is_closed is True 104 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 105 | batch_call = await response.parse() 106 | assert_matches_type(BatchCallResponse, batch_call, path=["response"]) 107 | 108 | @parametrize 109 | async def test_streaming_response_create_batch_call(self, async_client: AsyncRetell) -> None: 110 | async with async_client.batch_call.with_streaming_response.create_batch_call( 111 | from_number="+14157774444", 112 | tasks=[{"to_number": "+12137774445"}], 113 | ) as response: 114 | assert not response.is_closed 115 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 116 | 117 | batch_call = await response.parse() 118 | assert_matches_type(BatchCallResponse, batch_call, path=["response"]) 119 | 120 | assert cast(Any, response.is_closed) is True 121 | -------------------------------------------------------------------------------- /tests/api_resources/test_concurrency.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | import os 6 | from typing import Any, cast 7 | 8 | import pytest 9 | 10 | from retell import Retell, AsyncRetell 11 | from tests.utils import assert_matches_type 12 | from retell.types import ConcurrencyRetrieveResponse 13 | 14 | base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") 15 | 16 | 17 | class TestConcurrency: 18 | parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) 19 | 20 | @parametrize 21 | def test_method_retrieve(self, client: Retell) -> None: 22 | concurrency = client.concurrency.retrieve() 23 | assert_matches_type(ConcurrencyRetrieveResponse, concurrency, path=["response"]) 24 | 25 | @parametrize 26 | def test_raw_response_retrieve(self, client: Retell) -> None: 27 | response = client.concurrency.with_raw_response.retrieve() 28 | 29 | assert response.is_closed is True 30 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 31 | concurrency = response.parse() 32 | assert_matches_type(ConcurrencyRetrieveResponse, concurrency, path=["response"]) 33 | 34 | @parametrize 35 | def test_streaming_response_retrieve(self, client: Retell) -> None: 36 | with client.concurrency.with_streaming_response.retrieve() as response: 37 | assert not response.is_closed 38 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 39 | 40 | concurrency = response.parse() 41 | assert_matches_type(ConcurrencyRetrieveResponse, concurrency, path=["response"]) 42 | 43 | assert cast(Any, response.is_closed) is True 44 | 45 | 46 | class TestAsyncConcurrency: 47 | parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) 48 | 49 | @parametrize 50 | async def test_method_retrieve(self, async_client: AsyncRetell) -> None: 51 | concurrency = await async_client.concurrency.retrieve() 52 | assert_matches_type(ConcurrencyRetrieveResponse, concurrency, path=["response"]) 53 | 54 | @parametrize 55 | async def test_raw_response_retrieve(self, async_client: AsyncRetell) -> None: 56 | response = await async_client.concurrency.with_raw_response.retrieve() 57 | 58 | assert response.is_closed is True 59 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 60 | concurrency = await response.parse() 61 | assert_matches_type(ConcurrencyRetrieveResponse, concurrency, path=["response"]) 62 | 63 | @parametrize 64 | async def test_streaming_response_retrieve(self, async_client: AsyncRetell) -> None: 65 | async with async_client.concurrency.with_streaming_response.retrieve() as response: 66 | assert not response.is_closed 67 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 68 | 69 | concurrency = await response.parse() 70 | assert_matches_type(ConcurrencyRetrieveResponse, concurrency, path=["response"]) 71 | 72 | assert cast(Any, response.is_closed) is True 73 | -------------------------------------------------------------------------------- /tests/api_resources/test_voice.py: -------------------------------------------------------------------------------- 1 | # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 | 3 | from __future__ import annotations 4 | 5 | import os 6 | from typing import Any, cast 7 | 8 | import pytest 9 | 10 | from retell import Retell, AsyncRetell 11 | from tests.utils import assert_matches_type 12 | from retell.types import VoiceResponse, VoiceListResponse 13 | 14 | base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") 15 | 16 | 17 | class TestVoice: 18 | parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) 19 | 20 | @parametrize 21 | def test_method_retrieve(self, client: Retell) -> None: 22 | voice = client.voice.retrieve( 23 | "11labs-Adrian", 24 | ) 25 | assert_matches_type(VoiceResponse, voice, path=["response"]) 26 | 27 | @parametrize 28 | def test_raw_response_retrieve(self, client: Retell) -> None: 29 | response = client.voice.with_raw_response.retrieve( 30 | "11labs-Adrian", 31 | ) 32 | 33 | assert response.is_closed is True 34 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 35 | voice = response.parse() 36 | assert_matches_type(VoiceResponse, voice, path=["response"]) 37 | 38 | @parametrize 39 | def test_streaming_response_retrieve(self, client: Retell) -> None: 40 | with client.voice.with_streaming_response.retrieve( 41 | "11labs-Adrian", 42 | ) as response: 43 | assert not response.is_closed 44 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 45 | 46 | voice = response.parse() 47 | assert_matches_type(VoiceResponse, voice, path=["response"]) 48 | 49 | assert cast(Any, response.is_closed) is True 50 | 51 | @parametrize 52 | def test_path_params_retrieve(self, client: Retell) -> None: 53 | with pytest.raises(ValueError, match=r"Expected a non-empty value for `voice_id` but received ''"): 54 | client.voice.with_raw_response.retrieve( 55 | "", 56 | ) 57 | 58 | @parametrize 59 | def test_method_list(self, client: Retell) -> None: 60 | voice = client.voice.list() 61 | assert_matches_type(VoiceListResponse, voice, path=["response"]) 62 | 63 | @parametrize 64 | def test_raw_response_list(self, client: Retell) -> None: 65 | response = client.voice.with_raw_response.list() 66 | 67 | assert response.is_closed is True 68 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 69 | voice = response.parse() 70 | assert_matches_type(VoiceListResponse, voice, path=["response"]) 71 | 72 | @parametrize 73 | def test_streaming_response_list(self, client: Retell) -> None: 74 | with client.voice.with_streaming_response.list() as response: 75 | assert not response.is_closed 76 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 77 | 78 | voice = response.parse() 79 | assert_matches_type(VoiceListResponse, voice, path=["response"]) 80 | 81 | assert cast(Any, response.is_closed) is True 82 | 83 | 84 | class TestAsyncVoice: 85 | parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) 86 | 87 | @parametrize 88 | async def test_method_retrieve(self, async_client: AsyncRetell) -> None: 89 | voice = await async_client.voice.retrieve( 90 | "11labs-Adrian", 91 | ) 92 | assert_matches_type(VoiceResponse, voice, path=["response"]) 93 | 94 | @parametrize 95 | async def test_raw_response_retrieve(self, async_client: AsyncRetell) -> None: 96 | response = await async_client.voice.with_raw_response.retrieve( 97 | "11labs-Adrian", 98 | ) 99 | 100 | assert response.is_closed is True 101 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 102 | voice = await response.parse() 103 | assert_matches_type(VoiceResponse, voice, path=["response"]) 104 | 105 | @parametrize 106 | async def test_streaming_response_retrieve(self, async_client: AsyncRetell) -> None: 107 | async with async_client.voice.with_streaming_response.retrieve( 108 | "11labs-Adrian", 109 | ) as response: 110 | assert not response.is_closed 111 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 112 | 113 | voice = await response.parse() 114 | assert_matches_type(VoiceResponse, voice, path=["response"]) 115 | 116 | assert cast(Any, response.is_closed) is True 117 | 118 | @parametrize 119 | async def test_path_params_retrieve(self, async_client: AsyncRetell) -> None: 120 | with pytest.raises(ValueError, match=r"Expected a non-empty value for `voice_id` but received ''"): 121 | await async_client.voice.with_raw_response.retrieve( 122 | "", 123 | ) 124 | 125 | @parametrize 126 | async def test_method_list(self, async_client: AsyncRetell) -> None: 127 | voice = await async_client.voice.list() 128 | assert_matches_type(VoiceListResponse, voice, path=["response"]) 129 | 130 | @parametrize 131 | async def test_raw_response_list(self, async_client: AsyncRetell) -> None: 132 | response = await async_client.voice.with_raw_response.list() 133 | 134 | assert response.is_closed is True 135 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 136 | voice = await response.parse() 137 | assert_matches_type(VoiceListResponse, voice, path=["response"]) 138 | 139 | @parametrize 140 | async def test_streaming_response_list(self, async_client: AsyncRetell) -> None: 141 | async with async_client.voice.with_streaming_response.list() as response: 142 | assert not response.is_closed 143 | assert response.http_request.headers.get("X-Stainless-Lang") == "python" 144 | 145 | voice = await response.parse() 146 | assert_matches_type(VoiceListResponse, voice, path=["response"]) 147 | 148 | assert cast(Any, response.is_closed) is True 149 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import logging 5 | from typing import TYPE_CHECKING, Iterator, AsyncIterator 6 | 7 | import pytest 8 | from pytest_asyncio import is_async_test 9 | 10 | from retell import Retell, AsyncRetell 11 | 12 | if TYPE_CHECKING: 13 | from _pytest.fixtures import FixtureRequest # pyright: ignore[reportPrivateImportUsage] 14 | 15 | pytest.register_assert_rewrite("tests.utils") 16 | 17 | logging.getLogger("retell").setLevel(logging.DEBUG) 18 | 19 | 20 | # automatically add `pytest.mark.asyncio()` to all of our async tests 21 | # so we don't have to add that boilerplate everywhere 22 | def pytest_collection_modifyitems(items: list[pytest.Function]) -> None: 23 | pytest_asyncio_tests = (item for item in items if is_async_test(item)) 24 | session_scope_marker = pytest.mark.asyncio(loop_scope="session") 25 | for async_test in pytest_asyncio_tests: 26 | async_test.add_marker(session_scope_marker, append=False) 27 | 28 | 29 | base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") 30 | 31 | api_key = "YOUR_RETELL_API_KEY" 32 | 33 | 34 | @pytest.fixture(scope="session") 35 | def client(request: FixtureRequest) -> Iterator[Retell]: 36 | strict = getattr(request, "param", True) 37 | if not isinstance(strict, bool): 38 | raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") 39 | 40 | with Retell(base_url=base_url, api_key=api_key, _strict_response_validation=strict) as client: 41 | yield client 42 | 43 | 44 | @pytest.fixture(scope="session") 45 | async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncRetell]: 46 | strict = getattr(request, "param", True) 47 | if not isinstance(strict, bool): 48 | raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") 49 | 50 | async with AsyncRetell(base_url=base_url, api_key=api_key, _strict_response_validation=strict) as client: 51 | yield client 52 | -------------------------------------------------------------------------------- /tests/sample_file.txt: -------------------------------------------------------------------------------- 1 | Hello, world! 2 | -------------------------------------------------------------------------------- /tests/test_deepcopy.py: -------------------------------------------------------------------------------- 1 | from retell._utils import deepcopy_minimal 2 | 3 | 4 | def assert_different_identities(obj1: object, obj2: object) -> None: 5 | assert obj1 == obj2 6 | assert id(obj1) != id(obj2) 7 | 8 | 9 | def test_simple_dict() -> None: 10 | obj1 = {"foo": "bar"} 11 | obj2 = deepcopy_minimal(obj1) 12 | assert_different_identities(obj1, obj2) 13 | 14 | 15 | def test_nested_dict() -> None: 16 | obj1 = {"foo": {"bar": True}} 17 | obj2 = deepcopy_minimal(obj1) 18 | assert_different_identities(obj1, obj2) 19 | assert_different_identities(obj1["foo"], obj2["foo"]) 20 | 21 | 22 | def test_complex_nested_dict() -> None: 23 | obj1 = {"foo": {"bar": [{"hello": "world"}]}} 24 | obj2 = deepcopy_minimal(obj1) 25 | assert_different_identities(obj1, obj2) 26 | assert_different_identities(obj1["foo"], obj2["foo"]) 27 | assert_different_identities(obj1["foo"]["bar"], obj2["foo"]["bar"]) 28 | assert_different_identities(obj1["foo"]["bar"][0], obj2["foo"]["bar"][0]) 29 | 30 | 31 | def test_simple_list() -> None: 32 | obj1 = ["a", "b", "c"] 33 | obj2 = deepcopy_minimal(obj1) 34 | assert_different_identities(obj1, obj2) 35 | 36 | 37 | def test_nested_list() -> None: 38 | obj1 = ["a", [1, 2, 3]] 39 | obj2 = deepcopy_minimal(obj1) 40 | assert_different_identities(obj1, obj2) 41 | assert_different_identities(obj1[1], obj2[1]) 42 | 43 | 44 | class MyObject: ... 45 | 46 | 47 | def test_ignores_other_types() -> None: 48 | # custom classes 49 | my_obj = MyObject() 50 | obj1 = {"foo": my_obj} 51 | obj2 = deepcopy_minimal(obj1) 52 | assert_different_identities(obj1, obj2) 53 | assert obj1["foo"] is my_obj 54 | 55 | # tuples 56 | obj3 = ("a", "b") 57 | obj4 = deepcopy_minimal(obj3) 58 | assert obj3 is obj4 59 | -------------------------------------------------------------------------------- /tests/test_extract_files.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Sequence 4 | 5 | import pytest 6 | 7 | from retell._types import FileTypes 8 | from retell._utils import extract_files 9 | 10 | 11 | def test_removes_files_from_input() -> None: 12 | query = {"foo": "bar"} 13 | assert extract_files(query, paths=[]) == [] 14 | assert query == {"foo": "bar"} 15 | 16 | query2 = {"foo": b"Bar", "hello": "world"} 17 | assert extract_files(query2, paths=[["foo"]]) == [("foo", b"Bar")] 18 | assert query2 == {"hello": "world"} 19 | 20 | query3 = {"foo": {"foo": {"bar": b"Bar"}}, "hello": "world"} 21 | assert extract_files(query3, paths=[["foo", "foo", "bar"]]) == [("foo[foo][bar]", b"Bar")] 22 | assert query3 == {"foo": {"foo": {}}, "hello": "world"} 23 | 24 | query4 = {"foo": {"bar": b"Bar", "baz": "foo"}, "hello": "world"} 25 | assert extract_files(query4, paths=[["foo", "bar"]]) == [("foo[bar]", b"Bar")] 26 | assert query4 == {"hello": "world", "foo": {"baz": "foo"}} 27 | 28 | 29 | def test_multiple_files() -> None: 30 | query = {"documents": [{"file": b"My first file"}, {"file": b"My second file"}]} 31 | assert extract_files(query, paths=[["documents", "", "file"]]) == [ 32 | ("documents[file]", b"My first file"), 33 | ("documents[file]", b"My second file"), 34 | ] 35 | assert query == {"documents": [{}, {}]} 36 | 37 | 38 | @pytest.mark.parametrize( 39 | "query,paths,expected", 40 | [ 41 | [ 42 | {"foo": {"bar": "baz"}}, 43 | [["foo", "", "bar"]], 44 | [], 45 | ], 46 | [ 47 | {"foo": ["bar", "baz"]}, 48 | [["foo", "bar"]], 49 | [], 50 | ], 51 | [ 52 | {"foo": {"bar": "baz"}}, 53 | [["foo", "foo"]], 54 | [], 55 | ], 56 | ], 57 | ids=["dict expecting array", "array expecting dict", "unknown keys"], 58 | ) 59 | def test_ignores_incorrect_paths( 60 | query: dict[str, object], 61 | paths: Sequence[Sequence[str]], 62 | expected: list[tuple[str, FileTypes]], 63 | ) -> None: 64 | assert extract_files(query, paths=paths) == expected 65 | -------------------------------------------------------------------------------- /tests/test_files.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import anyio 4 | import pytest 5 | from dirty_equals import IsDict, IsList, IsBytes, IsTuple 6 | 7 | from retell._files import to_httpx_files, async_to_httpx_files 8 | 9 | readme_path = Path(__file__).parent.parent.joinpath("README.md") 10 | 11 | 12 | def test_pathlib_includes_file_name() -> None: 13 | result = to_httpx_files({"file": readme_path}) 14 | print(result) 15 | assert result == IsDict({"file": IsTuple("README.md", IsBytes())}) 16 | 17 | 18 | def test_tuple_input() -> None: 19 | result = to_httpx_files([("file", readme_path)]) 20 | print(result) 21 | assert result == IsList(IsTuple("file", IsTuple("README.md", IsBytes()))) 22 | 23 | 24 | @pytest.mark.asyncio 25 | async def test_async_pathlib_includes_file_name() -> None: 26 | result = await async_to_httpx_files({"file": readme_path}) 27 | print(result) 28 | assert result == IsDict({"file": IsTuple("README.md", IsBytes())}) 29 | 30 | 31 | @pytest.mark.asyncio 32 | async def test_async_supports_anyio_path() -> None: 33 | result = await async_to_httpx_files({"file": anyio.Path(readme_path)}) 34 | print(result) 35 | assert result == IsDict({"file": IsTuple("README.md", IsBytes())}) 36 | 37 | 38 | @pytest.mark.asyncio 39 | async def test_async_tuple_input() -> None: 40 | result = await async_to_httpx_files([("file", readme_path)]) 41 | print(result) 42 | assert result == IsList(IsTuple("file", IsTuple("README.md", IsBytes()))) 43 | 44 | 45 | def test_string_not_allowed() -> None: 46 | with pytest.raises(TypeError, match="Expected file types input to be a FileContent type or to be a tuple"): 47 | to_httpx_files( 48 | { 49 | "file": "foo", # type: ignore 50 | } 51 | ) 52 | -------------------------------------------------------------------------------- /tests/test_qs.py: -------------------------------------------------------------------------------- 1 | from typing import Any, cast 2 | from functools import partial 3 | from urllib.parse import unquote 4 | 5 | import pytest 6 | 7 | from retell._qs import Querystring, stringify 8 | 9 | 10 | def test_empty() -> None: 11 | assert stringify({}) == "" 12 | assert stringify({"a": {}}) == "" 13 | assert stringify({"a": {"b": {"c": {}}}}) == "" 14 | 15 | 16 | def test_basic() -> None: 17 | assert stringify({"a": 1}) == "a=1" 18 | assert stringify({"a": "b"}) == "a=b" 19 | assert stringify({"a": True}) == "a=true" 20 | assert stringify({"a": False}) == "a=false" 21 | assert stringify({"a": 1.23456}) == "a=1.23456" 22 | assert stringify({"a": None}) == "" 23 | 24 | 25 | @pytest.mark.parametrize("method", ["class", "function"]) 26 | def test_nested_dotted(method: str) -> None: 27 | if method == "class": 28 | serialise = Querystring(nested_format="dots").stringify 29 | else: 30 | serialise = partial(stringify, nested_format="dots") 31 | 32 | assert unquote(serialise({"a": {"b": "c"}})) == "a.b=c" 33 | assert unquote(serialise({"a": {"b": "c", "d": "e", "f": "g"}})) == "a.b=c&a.d=e&a.f=g" 34 | assert unquote(serialise({"a": {"b": {"c": {"d": "e"}}}})) == "a.b.c.d=e" 35 | assert unquote(serialise({"a": {"b": True}})) == "a.b=true" 36 | 37 | 38 | def test_nested_brackets() -> None: 39 | assert unquote(stringify({"a": {"b": "c"}})) == "a[b]=c" 40 | assert unquote(stringify({"a": {"b": "c", "d": "e", "f": "g"}})) == "a[b]=c&a[d]=e&a[f]=g" 41 | assert unquote(stringify({"a": {"b": {"c": {"d": "e"}}}})) == "a[b][c][d]=e" 42 | assert unquote(stringify({"a": {"b": True}})) == "a[b]=true" 43 | 44 | 45 | @pytest.mark.parametrize("method", ["class", "function"]) 46 | def test_array_comma(method: str) -> None: 47 | if method == "class": 48 | serialise = Querystring(array_format="comma").stringify 49 | else: 50 | serialise = partial(stringify, array_format="comma") 51 | 52 | assert unquote(serialise({"in": ["foo", "bar"]})) == "in=foo,bar" 53 | assert unquote(serialise({"a": {"b": [True, False]}})) == "a[b]=true,false" 54 | assert unquote(serialise({"a": {"b": [True, False, None, True]}})) == "a[b]=true,false,true" 55 | 56 | 57 | def test_array_repeat() -> None: 58 | assert unquote(stringify({"in": ["foo", "bar"]})) == "in=foo&in=bar" 59 | assert unquote(stringify({"a": {"b": [True, False]}})) == "a[b]=true&a[b]=false" 60 | assert unquote(stringify({"a": {"b": [True, False, None, True]}})) == "a[b]=true&a[b]=false&a[b]=true" 61 | assert unquote(stringify({"in": ["foo", {"b": {"c": ["d", "e"]}}]})) == "in=foo&in[b][c]=d&in[b][c]=e" 62 | 63 | 64 | @pytest.mark.parametrize("method", ["class", "function"]) 65 | def test_array_brackets(method: str) -> None: 66 | if method == "class": 67 | serialise = Querystring(array_format="brackets").stringify 68 | else: 69 | serialise = partial(stringify, array_format="brackets") 70 | 71 | assert unquote(serialise({"in": ["foo", "bar"]})) == "in[]=foo&in[]=bar" 72 | assert unquote(serialise({"a": {"b": [True, False]}})) == "a[b][]=true&a[b][]=false" 73 | assert unquote(serialise({"a": {"b": [True, False, None, True]}})) == "a[b][]=true&a[b][]=false&a[b][]=true" 74 | 75 | 76 | def test_unknown_array_format() -> None: 77 | with pytest.raises(NotImplementedError, match="Unknown array_format value: foo, choose from comma, repeat"): 78 | stringify({"a": ["foo", "bar"]}, array_format=cast(Any, "foo")) 79 | -------------------------------------------------------------------------------- /tests/test_required_args.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from retell._utils import required_args 6 | 7 | 8 | def test_too_many_positional_params() -> None: 9 | @required_args(["a"]) 10 | def foo(a: str | None = None) -> str | None: 11 | return a 12 | 13 | with pytest.raises(TypeError, match=r"foo\(\) takes 1 argument\(s\) but 2 were given"): 14 | foo("a", "b") # type: ignore 15 | 16 | 17 | def test_positional_param() -> None: 18 | @required_args(["a"]) 19 | def foo(a: str | None = None) -> str | None: 20 | return a 21 | 22 | assert foo("a") == "a" 23 | assert foo(None) is None 24 | assert foo(a="b") == "b" 25 | 26 | with pytest.raises(TypeError, match="Missing required argument: 'a'"): 27 | foo() 28 | 29 | 30 | def test_keyword_only_param() -> None: 31 | @required_args(["a"]) 32 | def foo(*, a: str | None = None) -> str | None: 33 | return a 34 | 35 | assert foo(a="a") == "a" 36 | assert foo(a=None) is None 37 | assert foo(a="b") == "b" 38 | 39 | with pytest.raises(TypeError, match="Missing required argument: 'a'"): 40 | foo() 41 | 42 | 43 | def test_multiple_params() -> None: 44 | @required_args(["a", "b", "c"]) 45 | def foo(a: str = "", *, b: str = "", c: str = "") -> str | None: 46 | return f"{a} {b} {c}" 47 | 48 | assert foo(a="a", b="b", c="c") == "a b c" 49 | 50 | error_message = r"Missing required arguments.*" 51 | 52 | with pytest.raises(TypeError, match=error_message): 53 | foo() 54 | 55 | with pytest.raises(TypeError, match=error_message): 56 | foo(a="a") 57 | 58 | with pytest.raises(TypeError, match=error_message): 59 | foo(b="b") 60 | 61 | with pytest.raises(TypeError, match=error_message): 62 | foo(c="c") 63 | 64 | with pytest.raises(TypeError, match=r"Missing required argument: 'a'"): 65 | foo(b="a", c="c") 66 | 67 | with pytest.raises(TypeError, match=r"Missing required argument: 'b'"): 68 | foo("a", c="c") 69 | 70 | 71 | def test_multiple_variants() -> None: 72 | @required_args(["a"], ["b"]) 73 | def foo(*, a: str | None = None, b: str | None = None) -> str | None: 74 | return a if a is not None else b 75 | 76 | assert foo(a="foo") == "foo" 77 | assert foo(b="bar") == "bar" 78 | assert foo(a=None) is None 79 | assert foo(b=None) is None 80 | 81 | # TODO: this error message could probably be improved 82 | with pytest.raises( 83 | TypeError, 84 | match=r"Missing required arguments; Expected either \('a'\) or \('b'\) arguments to be given", 85 | ): 86 | foo() 87 | 88 | 89 | def test_multiple_params_multiple_variants() -> None: 90 | @required_args(["a", "b"], ["c"]) 91 | def foo(*, a: str | None = None, b: str | None = None, c: str | None = None) -> str | None: 92 | if a is not None: 93 | return a 94 | if b is not None: 95 | return b 96 | return c 97 | 98 | error_message = r"Missing required arguments; Expected either \('a' and 'b'\) or \('c'\) arguments to be given" 99 | 100 | with pytest.raises(TypeError, match=error_message): 101 | foo(a="foo") 102 | 103 | with pytest.raises(TypeError, match=error_message): 104 | foo(b="bar") 105 | 106 | with pytest.raises(TypeError, match=error_message): 107 | foo() 108 | 109 | assert foo(a=None, b="bar") == "bar" 110 | assert foo(c=None) is None 111 | assert foo(c="foo") == "foo" 112 | -------------------------------------------------------------------------------- /tests/test_streaming.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Iterator, AsyncIterator 4 | 5 | import httpx 6 | import pytest 7 | 8 | from retell import Retell, AsyncRetell 9 | from retell._streaming import Stream, AsyncStream, ServerSentEvent 10 | 11 | 12 | @pytest.mark.asyncio 13 | @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) 14 | async def test_basic(sync: bool, client: Retell, async_client: AsyncRetell) -> None: 15 | def body() -> Iterator[bytes]: 16 | yield b"event: completion\n" 17 | yield b'data: {"foo":true}\n' 18 | yield b"\n" 19 | 20 | iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) 21 | 22 | sse = await iter_next(iterator) 23 | assert sse.event == "completion" 24 | assert sse.json() == {"foo": True} 25 | 26 | await assert_empty_iter(iterator) 27 | 28 | 29 | @pytest.mark.asyncio 30 | @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) 31 | async def test_data_missing_event(sync: bool, client: Retell, async_client: AsyncRetell) -> None: 32 | def body() -> Iterator[bytes]: 33 | yield b'data: {"foo":true}\n' 34 | yield b"\n" 35 | 36 | iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) 37 | 38 | sse = await iter_next(iterator) 39 | assert sse.event is None 40 | assert sse.json() == {"foo": True} 41 | 42 | await assert_empty_iter(iterator) 43 | 44 | 45 | @pytest.mark.asyncio 46 | @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) 47 | async def test_event_missing_data(sync: bool, client: Retell, async_client: AsyncRetell) -> None: 48 | def body() -> Iterator[bytes]: 49 | yield b"event: ping\n" 50 | yield b"\n" 51 | 52 | iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) 53 | 54 | sse = await iter_next(iterator) 55 | assert sse.event == "ping" 56 | assert sse.data == "" 57 | 58 | await assert_empty_iter(iterator) 59 | 60 | 61 | @pytest.mark.asyncio 62 | @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) 63 | async def test_multiple_events(sync: bool, client: Retell, async_client: AsyncRetell) -> None: 64 | def body() -> Iterator[bytes]: 65 | yield b"event: ping\n" 66 | yield b"\n" 67 | yield b"event: completion\n" 68 | yield b"\n" 69 | 70 | iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) 71 | 72 | sse = await iter_next(iterator) 73 | assert sse.event == "ping" 74 | assert sse.data == "" 75 | 76 | sse = await iter_next(iterator) 77 | assert sse.event == "completion" 78 | assert sse.data == "" 79 | 80 | await assert_empty_iter(iterator) 81 | 82 | 83 | @pytest.mark.asyncio 84 | @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) 85 | async def test_multiple_events_with_data(sync: bool, client: Retell, async_client: AsyncRetell) -> None: 86 | def body() -> Iterator[bytes]: 87 | yield b"event: ping\n" 88 | yield b'data: {"foo":true}\n' 89 | yield b"\n" 90 | yield b"event: completion\n" 91 | yield b'data: {"bar":false}\n' 92 | yield b"\n" 93 | 94 | iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) 95 | 96 | sse = await iter_next(iterator) 97 | assert sse.event == "ping" 98 | assert sse.json() == {"foo": True} 99 | 100 | sse = await iter_next(iterator) 101 | assert sse.event == "completion" 102 | assert sse.json() == {"bar": False} 103 | 104 | await assert_empty_iter(iterator) 105 | 106 | 107 | @pytest.mark.asyncio 108 | @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) 109 | async def test_multiple_data_lines_with_empty_line(sync: bool, client: Retell, async_client: AsyncRetell) -> None: 110 | def body() -> Iterator[bytes]: 111 | yield b"event: ping\n" 112 | yield b"data: {\n" 113 | yield b'data: "foo":\n' 114 | yield b"data: \n" 115 | yield b"data:\n" 116 | yield b"data: true}\n" 117 | yield b"\n\n" 118 | 119 | iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) 120 | 121 | sse = await iter_next(iterator) 122 | assert sse.event == "ping" 123 | assert sse.json() == {"foo": True} 124 | assert sse.data == '{\n"foo":\n\n\ntrue}' 125 | 126 | await assert_empty_iter(iterator) 127 | 128 | 129 | @pytest.mark.asyncio 130 | @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) 131 | async def test_data_json_escaped_double_new_line(sync: bool, client: Retell, async_client: AsyncRetell) -> None: 132 | def body() -> Iterator[bytes]: 133 | yield b"event: ping\n" 134 | yield b'data: {"foo": "my long\\n\\ncontent"}' 135 | yield b"\n\n" 136 | 137 | iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) 138 | 139 | sse = await iter_next(iterator) 140 | assert sse.event == "ping" 141 | assert sse.json() == {"foo": "my long\n\ncontent"} 142 | 143 | await assert_empty_iter(iterator) 144 | 145 | 146 | @pytest.mark.asyncio 147 | @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) 148 | async def test_multiple_data_lines(sync: bool, client: Retell, async_client: AsyncRetell) -> None: 149 | def body() -> Iterator[bytes]: 150 | yield b"event: ping\n" 151 | yield b"data: {\n" 152 | yield b'data: "foo":\n' 153 | yield b"data: true}\n" 154 | yield b"\n\n" 155 | 156 | iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) 157 | 158 | sse = await iter_next(iterator) 159 | assert sse.event == "ping" 160 | assert sse.json() == {"foo": True} 161 | 162 | await assert_empty_iter(iterator) 163 | 164 | 165 | @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) 166 | async def test_special_new_line_character( 167 | sync: bool, 168 | client: Retell, 169 | async_client: AsyncRetell, 170 | ) -> None: 171 | def body() -> Iterator[bytes]: 172 | yield b'data: {"content":" culpa"}\n' 173 | yield b"\n" 174 | yield b'data: {"content":" \xe2\x80\xa8"}\n' 175 | yield b"\n" 176 | yield b'data: {"content":"foo"}\n' 177 | yield b"\n" 178 | 179 | iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) 180 | 181 | sse = await iter_next(iterator) 182 | assert sse.event is None 183 | assert sse.json() == {"content": " culpa"} 184 | 185 | sse = await iter_next(iterator) 186 | assert sse.event is None 187 | assert sse.json() == {"content": " 
"} 188 | 189 | sse = await iter_next(iterator) 190 | assert sse.event is None 191 | assert sse.json() == {"content": "foo"} 192 | 193 | await assert_empty_iter(iterator) 194 | 195 | 196 | @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) 197 | async def test_multi_byte_character_multiple_chunks( 198 | sync: bool, 199 | client: Retell, 200 | async_client: AsyncRetell, 201 | ) -> None: 202 | def body() -> Iterator[bytes]: 203 | yield b'data: {"content":"' 204 | # bytes taken from the string 'известни' and arbitrarily split 205 | # so that some multi-byte characters span multiple chunks 206 | yield b"\xd0" 207 | yield b"\xb8\xd0\xb7\xd0" 208 | yield b"\xb2\xd0\xb5\xd1\x81\xd1\x82\xd0\xbd\xd0\xb8" 209 | yield b'"}\n' 210 | yield b"\n" 211 | 212 | iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) 213 | 214 | sse = await iter_next(iterator) 215 | assert sse.event is None 216 | assert sse.json() == {"content": "известни"} 217 | 218 | 219 | async def to_aiter(iter: Iterator[bytes]) -> AsyncIterator[bytes]: 220 | for chunk in iter: 221 | yield chunk 222 | 223 | 224 | async def iter_next(iter: Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]) -> ServerSentEvent: 225 | if isinstance(iter, AsyncIterator): 226 | return await iter.__anext__() 227 | 228 | return next(iter) 229 | 230 | 231 | async def assert_empty_iter(iter: Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]) -> None: 232 | with pytest.raises((StopAsyncIteration, RuntimeError)): 233 | await iter_next(iter) 234 | 235 | 236 | def make_event_iterator( 237 | content: Iterator[bytes], 238 | *, 239 | sync: bool, 240 | client: Retell, 241 | async_client: AsyncRetell, 242 | ) -> Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]: 243 | if sync: 244 | return Stream(cast_to=object, client=client, response=httpx.Response(200, content=content))._iter_events() 245 | 246 | return AsyncStream( 247 | cast_to=object, client=async_client, response=httpx.Response(200, content=to_aiter(content)) 248 | )._iter_events() 249 | -------------------------------------------------------------------------------- /tests/test_utils/test_proxy.py: -------------------------------------------------------------------------------- 1 | import operator 2 | from typing import Any 3 | from typing_extensions import override 4 | 5 | from retell._utils import LazyProxy 6 | 7 | 8 | class RecursiveLazyProxy(LazyProxy[Any]): 9 | @override 10 | def __load__(self) -> Any: 11 | return self 12 | 13 | def __call__(self, *_args: Any, **_kwds: Any) -> Any: 14 | raise RuntimeError("This should never be called!") 15 | 16 | 17 | def test_recursive_proxy() -> None: 18 | proxy = RecursiveLazyProxy() 19 | assert repr(proxy) == "RecursiveLazyProxy" 20 | assert str(proxy) == "RecursiveLazyProxy" 21 | assert dir(proxy) == [] 22 | assert type(proxy).__name__ == "RecursiveLazyProxy" 23 | assert type(operator.attrgetter("name.foo.bar.baz")(proxy)).__name__ == "RecursiveLazyProxy" 24 | 25 | 26 | def test_isinstance_does_not_error() -> None: 27 | class AlwaysErrorProxy(LazyProxy[Any]): 28 | @override 29 | def __load__(self) -> Any: 30 | raise RuntimeError("Mocking missing dependency") 31 | 32 | proxy = AlwaysErrorProxy() 33 | assert not isinstance(proxy, dict) 34 | assert isinstance(proxy, LazyProxy) 35 | -------------------------------------------------------------------------------- /tests/test_utils/test_typing.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Generic, TypeVar, cast 4 | 5 | from retell._utils import extract_type_var_from_base 6 | 7 | _T = TypeVar("_T") 8 | _T2 = TypeVar("_T2") 9 | _T3 = TypeVar("_T3") 10 | 11 | 12 | class BaseGeneric(Generic[_T]): ... 13 | 14 | 15 | class SubclassGeneric(BaseGeneric[_T]): ... 16 | 17 | 18 | class BaseGenericMultipleTypeArgs(Generic[_T, _T2, _T3]): ... 19 | 20 | 21 | class SubclassGenericMultipleTypeArgs(BaseGenericMultipleTypeArgs[_T, _T2, _T3]): ... 22 | 23 | 24 | class SubclassDifferentOrderGenericMultipleTypeArgs(BaseGenericMultipleTypeArgs[_T2, _T, _T3]): ... 25 | 26 | 27 | def test_extract_type_var() -> None: 28 | assert ( 29 | extract_type_var_from_base( 30 | BaseGeneric[int], 31 | index=0, 32 | generic_bases=cast("tuple[type, ...]", (BaseGeneric,)), 33 | ) 34 | == int 35 | ) 36 | 37 | 38 | def test_extract_type_var_generic_subclass() -> None: 39 | assert ( 40 | extract_type_var_from_base( 41 | SubclassGeneric[int], 42 | index=0, 43 | generic_bases=cast("tuple[type, ...]", (BaseGeneric,)), 44 | ) 45 | == int 46 | ) 47 | 48 | 49 | def test_extract_type_var_multiple() -> None: 50 | typ = BaseGenericMultipleTypeArgs[int, str, None] 51 | 52 | generic_bases = cast("tuple[type, ...]", (BaseGenericMultipleTypeArgs,)) 53 | assert extract_type_var_from_base(typ, index=0, generic_bases=generic_bases) == int 54 | assert extract_type_var_from_base(typ, index=1, generic_bases=generic_bases) == str 55 | assert extract_type_var_from_base(typ, index=2, generic_bases=generic_bases) == type(None) 56 | 57 | 58 | def test_extract_type_var_generic_subclass_multiple() -> None: 59 | typ = SubclassGenericMultipleTypeArgs[int, str, None] 60 | 61 | generic_bases = cast("tuple[type, ...]", (BaseGenericMultipleTypeArgs,)) 62 | assert extract_type_var_from_base(typ, index=0, generic_bases=generic_bases) == int 63 | assert extract_type_var_from_base(typ, index=1, generic_bases=generic_bases) == str 64 | assert extract_type_var_from_base(typ, index=2, generic_bases=generic_bases) == type(None) 65 | 66 | 67 | def test_extract_type_var_generic_subclass_different_ordering_multiple() -> None: 68 | typ = SubclassDifferentOrderGenericMultipleTypeArgs[int, str, None] 69 | 70 | generic_bases = cast("tuple[type, ...]", (BaseGenericMultipleTypeArgs,)) 71 | assert extract_type_var_from_base(typ, index=0, generic_bases=generic_bases) == int 72 | assert extract_type_var_from_base(typ, index=1, generic_bases=generic_bases) == str 73 | assert extract_type_var_from_base(typ, index=2, generic_bases=generic_bases) == type(None) 74 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import inspect 5 | import traceback 6 | import contextlib 7 | from typing import Any, TypeVar, Iterator, cast 8 | from datetime import date, datetime 9 | from typing_extensions import Literal, get_args, get_origin, assert_type 10 | 11 | from retell._types import Omit, NoneType 12 | from retell._utils import ( 13 | is_dict, 14 | is_list, 15 | is_list_type, 16 | is_union_type, 17 | extract_type_arg, 18 | is_annotated_type, 19 | is_type_alias_type, 20 | ) 21 | from retell._compat import PYDANTIC_V2, field_outer_type, get_model_fields 22 | from retell._models import BaseModel 23 | 24 | BaseModelT = TypeVar("BaseModelT", bound=BaseModel) 25 | 26 | 27 | def assert_matches_model(model: type[BaseModelT], value: BaseModelT, *, path: list[str]) -> bool: 28 | for name, field in get_model_fields(model).items(): 29 | field_value = getattr(value, name) 30 | if PYDANTIC_V2: 31 | allow_none = False 32 | else: 33 | # in v1 nullability was structured differently 34 | # https://docs.pydantic.dev/2.0/migration/#required-optional-and-nullable-fields 35 | allow_none = getattr(field, "allow_none", False) 36 | 37 | assert_matches_type( 38 | field_outer_type(field), 39 | field_value, 40 | path=[*path, name], 41 | allow_none=allow_none, 42 | ) 43 | 44 | return True 45 | 46 | 47 | # Note: the `path` argument is only used to improve error messages when `--showlocals` is used 48 | def assert_matches_type( 49 | type_: Any, 50 | value: object, 51 | *, 52 | path: list[str], 53 | allow_none: bool = False, 54 | ) -> None: 55 | if is_type_alias_type(type_): 56 | type_ = type_.__value__ 57 | 58 | # unwrap `Annotated[T, ...]` -> `T` 59 | if is_annotated_type(type_): 60 | type_ = extract_type_arg(type_, 0) 61 | 62 | if allow_none and value is None: 63 | return 64 | 65 | if type_ is None or type_ is NoneType: 66 | assert value is None 67 | return 68 | 69 | origin = get_origin(type_) or type_ 70 | 71 | if is_list_type(type_): 72 | return _assert_list_type(type_, value) 73 | 74 | if origin == str: 75 | assert isinstance(value, str) 76 | elif origin == int: 77 | assert isinstance(value, int) 78 | elif origin == bool: 79 | assert isinstance(value, bool) 80 | elif origin == float: 81 | assert isinstance(value, float) 82 | elif origin == bytes: 83 | assert isinstance(value, bytes) 84 | elif origin == datetime: 85 | assert isinstance(value, datetime) 86 | elif origin == date: 87 | assert isinstance(value, date) 88 | elif origin == object: 89 | # nothing to do here, the expected type is unknown 90 | pass 91 | elif origin == Literal: 92 | assert value in get_args(type_) 93 | elif origin == dict: 94 | assert is_dict(value) 95 | 96 | args = get_args(type_) 97 | key_type = args[0] 98 | items_type = args[1] 99 | 100 | for key, item in value.items(): 101 | assert_matches_type(key_type, key, path=[*path, ""]) 102 | assert_matches_type(items_type, item, path=[*path, ""]) 103 | elif is_union_type(type_): 104 | variants = get_args(type_) 105 | 106 | try: 107 | none_index = variants.index(type(None)) 108 | except ValueError: 109 | pass 110 | else: 111 | # special case Optional[T] for better error messages 112 | if len(variants) == 2: 113 | if value is None: 114 | # valid 115 | return 116 | 117 | return assert_matches_type(type_=variants[not none_index], value=value, path=path) 118 | 119 | for i, variant in enumerate(variants): 120 | try: 121 | assert_matches_type(variant, value, path=[*path, f"variant {i}"]) 122 | return 123 | except AssertionError: 124 | traceback.print_exc() 125 | continue 126 | 127 | raise AssertionError("Did not match any variants") 128 | elif issubclass(origin, BaseModel): 129 | assert isinstance(value, type_) 130 | assert assert_matches_model(type_, cast(Any, value), path=path) 131 | elif inspect.isclass(origin) and origin.__name__ == "HttpxBinaryResponseContent": 132 | assert value.__class__.__name__ == "HttpxBinaryResponseContent" 133 | else: 134 | assert None, f"Unhandled field type: {type_}" 135 | 136 | 137 | def _assert_list_type(type_: type[object], value: object) -> None: 138 | assert is_list(value) 139 | 140 | inner_type = get_args(type_)[0] 141 | for entry in value: 142 | assert_type(inner_type, entry) # type: ignore 143 | 144 | 145 | @contextlib.contextmanager 146 | def update_env(**new_env: str | Omit) -> Iterator[None]: 147 | old = os.environ.copy() 148 | 149 | try: 150 | for name, value in new_env.items(): 151 | if isinstance(value, Omit): 152 | os.environ.pop(name, None) 153 | else: 154 | os.environ[name] = value 155 | 156 | yield None 157 | finally: 158 | os.environ.clear() 159 | os.environ.update(old) 160 | -------------------------------------------------------------------------------- /tests/webhook_auth.py: -------------------------------------------------------------------------------- 1 | import json 2 | from retell.lib.webhook_auth import symmetric 3 | 4 | 5 | def test_symmetric_signature() -> None: 6 | post_data = { 7 | "name": "some_function_name", 8 | "args": {"some_arg": "ABC123"}, 9 | } 10 | body = json.dumps(post_data, separators=(",", ":"), ensure_ascii=False) 11 | api_key = "fake-api-key" 12 | 13 | signature = symmetric["sign"](body, api_key) 14 | assert symmetric["verify"](body, api_key, signature) 15 | --------------------------------------------------------------------------------