diff --git a/.fern/metadata.json b/.fern/metadata.json new file mode 100644 index 0000000..c81f513 --- /dev/null +++ b/.fern/metadata.json @@ -0,0 +1,16 @@ +{ + "cliVersion": "5.50.3", + "generatorName": "fernapi/fern-python-sdk", + "generatorVersion": "5.14.20", + "generatorConfig": { + "package_name": "speechify", + "client": { + "class_name": "Speechify" + }, + "pyproject_python_version": ">=3.9" + }, + "originGitCommit": "13f26db544c7ebda83d5d229fb387a34c8410ee0", + "originGitCommitIsDirty": true, + "invokedBy": "manual", + "sdkVersion": "1.2.4" +} \ No newline at end of file diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 19d852a..3cd7f21 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -18,7 +18,7 @@ jobs: - name: Set up python uses: actions/setup-python@v5 with: - python-version: "3.8" + python-version: "3.10" - name: Bootstrap poetry run: | curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 @@ -35,7 +35,7 @@ jobs: - name: Set up python uses: actions/setup-python@v5 with: - python-version: "3.8" + python-version: "3.10" - name: Bootstrap poetry run: | curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 @@ -73,7 +73,7 @@ jobs: - name: Set up python uses: actions/setup-python@v5 with: - python-version: "3.8" + python-version: "3.10" - name: Bootstrap poetry run: | curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 diff --git a/.gitignore b/.gitignore index 0da665f..d2e4ca8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ -dist/ .mypy_cache/ +.ruff_cache/ __pycache__/ +dist/ poetry.toml -.ruff_cache/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..af948ce --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,125 @@ +# Contributing + +Thanks for your interest in contributing to this SDK! This document provides guidelines for contributing to the project. + +## Getting Started + +### Prerequisites + +- Python 3.9+ +- pip +- poetry + +### Installation + +Install the project dependencies: + +```bash +poetry install +``` + +### Building + +Build the project: + +```bash +poetry build +``` + +### Testing + +Run the test suite: + +```bash +poetry run pytest +``` + +### Linting and Formatting + +Check code style: + +```bash +poetry run ruff check . +poetry run ruff format . +``` + +### Type Checking + +Run the type checker: + +```bash +poetry run mypy . +``` + +## About Generated Code + +**Important**: Most files in this SDK are automatically generated by [Fern](https://buildwithfern.com) from the API definition. Direct modifications to generated files will be overwritten the next time the SDK is generated. + +### Generated Files + +The following directories contain generated code: +- `src/` - API client classes and types +- Most Python files in the project + +### How to Customize + +If you need to customize the SDK, you have two options: + +#### Option 1: Use `.fernignore` + +For custom code that should persist across SDK regenerations: + +1. Create a `.fernignore` file in the project root +2. Add file patterns for files you want to preserve (similar to `.gitignore` syntax) +3. Add your custom code to those files + +Files listed in `.fernignore` will not be overwritten when the SDK is regenerated. + +For more information, see the [Fern documentation on custom code](https://buildwithfern.com/learn/sdks/overview/custom-code). + +#### Option 2: Contribute to the Generator + +If you want to change how code is generated for all users of this SDK: + +1. The Python SDK generator lives in the [Fern repository](https://github.com/fern-api/fern) +2. Generator code is located at `generators/python-v2/` +3. Follow the [Fern contributing guidelines](https://github.com/fern-api/fern/blob/main/CONTRIBUTING.md) +4. Submit a pull request with your changes to the generator + +This approach is best for: +- Bug fixes in generated code +- New features that would benefit all users +- Improvements to code generation patterns + +## Making Changes + +### Workflow + +1. Create a new branch for your changes +2. Make your modifications +3. Run tests to ensure nothing breaks: `poetry run pytest` +4. Run linting and formatting: `poetry run ruff check .` and `poetry run ruff format .` +5. Run type checking: `poetry run mypy .` +6. Build the project: `poetry build` +7. Commit your changes with a clear commit message +8. Push your branch and create a pull request + +### Commit Messages + +Write clear, descriptive commit messages that explain what changed and why. + +### Code Style + +This project uses automated code formatting and linting. Run `poetry run ruff format .` and `poetry run ruff check .` before committing to ensure your code meets the project's style guidelines. + +## Questions or Issues? + +If you have questions or run into issues: + +1. Check the [Fern documentation](https://buildwithfern.com) +2. Search existing [GitHub issues](https://github.com/fern-api/fern/issues) +3. Open a new issue if your question hasn't been addressed + +## License + +By contributing to this project, you agree that your contributions will be licensed under the same license as the project. diff --git a/README.md b/README.md index 7291477..81877f0 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,11 @@ The Speechifyinc Python library provides convenient access to the Speechifyinc A - [Installation](#installation) - [Reference](#reference) - [Usage](#usage) +- [Environments](#environments) - [Async Client](#async-client) - [Exception Handling](#exception-handling) - [Advanced](#advanced) + - [Access Raw Response Data](#access-raw-response-data) - [Retries](#retries) - [Timeouts](#timeouts) - [Custom Client](#custom-client) @@ -41,17 +43,33 @@ Instantiate and use the client with the following: from speechify import Speechify client = Speechify( - token="YOUR_TOKEN", + api_key="", ) -client.tts.audio.speech( - input="input", - voice_id="voice_id", + +client.audio.speech( + audio_format="mp3", + input="Hello! This is the Speechify text-to-speech API.", + model="simba-english", + voice_id="george", +) +``` + +## Environments + +This SDK allows you to configure different environments for API requests. + +```python +from speechify import Speechify +from speechify.environment import SpeechifyEnvironment + +client = Speechify( + environment=SpeechifyEnvironment.DEFAULT, ) ``` ## Async Client -The SDK also exports an `async` client so that you can make non-blocking calls to our API. +The SDK also exports an `async` client so that you can make non-blocking calls to our API. Note that if you are constructing an Async httpx client class to pass into this client, use `httpx.AsyncClient()` instead of `httpx.Client()` (e.g. for the `httpx_client` parameter of this client). ```python import asyncio @@ -59,14 +77,16 @@ import asyncio from speechify import AsyncSpeechify client = AsyncSpeechify( - token="YOUR_TOKEN", + api_key="", ) async def main() -> None: - await client.tts.audio.speech( - input="input", - voice_id="voice_id", + await client.audio.speech( + audio_format="mp3", + input="Hello! This is the Speechify text-to-speech API.", + model="simba-english", + voice_id="george", ) @@ -82,7 +102,7 @@ will be thrown. from speechify.core.api_error import ApiError try: - client.tts.audio.speech(...) + client.audio.speech(...) except ApiError as e: print(e.status_code) print(e.body) @@ -90,22 +110,47 @@ except ApiError as e: ## Advanced +### Access Raw Response Data + +The SDK provides access to raw response data, including headers, through the `.with_raw_response` property. +The `.with_raw_response` property returns a "raw" client that can be used to access the `.headers` and `.data` attributes. + +```python +from speechify import Speechify + +client = Speechify(...) +response = client.audio.with_raw_response.speech(...) +print(response.headers) # access the response headers +print(response.status_code) # access the response status code +print(response.data) # access the underlying object +``` + ### Retries The SDK is instrumented with automatic retries with exponential backoff. A request will be retried as long as the request is deemed retryable and the number of retry attempts has not grown larger than the configured retry limit (default: 2). -A request is deemed retryable when any of the following HTTP status codes is returned: +Which status codes are retried depends on the `retryStatusCodes` generator configuration: + +**`legacy`** (current default): retries on +- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout) +- [409](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409) (Conflict) +- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests) +- [5XX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#server_error_responses) (All server errors, including 500) +**`recommended`**: retries on - [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout) +- [409](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409) (Conflict) - [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests) -- [5XX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500) (Internal Server Errors) +- [502](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502) (Bad Gateway) +- [503](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503) (Service Unavailable) +- [504](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504) (Gateway Timeout) Use the `max_retries` request option to configure this behavior. ```python -client.tts.audio.speech(..., request_options={ +client.audio.speech(..., request_options={ "max_retries": 1 }) ``` @@ -115,17 +160,12 @@ client.tts.audio.speech(..., request_options={ The SDK defaults to a 60 second timeout. You can configure this with a timeout option at the client or request level. ```python - from speechify import Speechify -client = Speechify( - ..., - timeout=20.0, -) - +client = Speechify(..., timeout=20.0) # Override timeout for a specific method -client.tts.audio.speech(..., request_options={ +client.audio.speech(..., request_options={ "timeout_in_seconds": 1 }) ``` @@ -134,6 +174,7 @@ client.tts.audio.speech(..., request_options={ You can override the `httpx` client to customize it for your use-case. Some common use-cases include support for proxies and transports. + ```python import httpx from speechify import Speechify @@ -141,7 +182,7 @@ from speechify import Speechify client = Speechify( ..., httpx_client=httpx.Client( - proxies="http://my.test.proxy.example.com", + proxy="http://my.test.proxy.example.com", transport=httpx.HTTPTransport(local_address="0.0.0.0"), ), ) diff --git a/context7.json b/context7.json deleted file mode 100644 index 57f4636..0000000 --- a/context7.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "url": "https://context7.com/speechifyinc/speechify-api-sdk-python", - "public_key": "pk_APO3PeuTVigMZ5P69udhE" -} diff --git a/poetry.lock b/poetry.lock index 1a4242d..bc07681 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,178 @@ -# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.2" +description = "Happy Eyeballs for asyncio" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"aiohttp\"" +files = [ + {file = "aiohappyeyeballs-2.6.2-py3-none-any.whl", hash = "sha256:4708045e2d7a6c6bdf8aafa8ed39649eaf926a4543b54560659129e3365953c4"}, + {file = "aiohappyeyeballs-2.6.2.tar.gz", hash = "sha256:e202810ee718bd01fc6ef49e8ea53d023d5cb6b581076d7925aa499fa55dbe64"}, +] + +[[package]] +name = "aiohttp" +version = "3.14.1" +description = "Async http client/server framework (asyncio)" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"aiohttp\"" +files = [ + {file = "aiohttp-3.14.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8f6bb621e5863cfe8fe5ff5468002d200ec31f30f1280b259dc505b02595099e"}, + {file = "aiohttp-3.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f7215cb3933784f79ed20e5f050e15984f390424339b22375d5a53c933a0491"}, + {file = "aiohttp-3.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9d4e294455b23a68c9b8f042d0e8e377a265bcb15332753695f6e5b6819e0ce"}, + {file = "aiohttp-3.14.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b238af795833d5731d049d82bc84b768ae6f8f97f0495963b3ed9935c5901cc3"}, + {file = "aiohttp-3.14.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4e5e0ae56914ecdbf446493addefc0159053dd53962cef37d7839f37f73d505"}, + {file = "aiohttp-3.14.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:092e4ce3619a7c6dee52a6bdabda973d9b34b66781f840ce93c7e0cec30cf521"}, + {file = "aiohttp-3.14.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb33777ea21e8b7ecde0e6fc84f598be0a1192eab1a63bc746d75aa75d38e7bd"}, + {file = "aiohttp-3.14.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23119f8fd4f5d16902ed459b63b100bcd269628075162bddac56cc7b5273b3fb"}, + {file = "aiohttp-3.14.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:57fc6745a4b7d0f5a9eb4f40a69718be6c0bc1b8368cc9fe89e90118719f4f42"}, + {file = "aiohttp-3.14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6fd35beba67c4183b09375c5fff9accb47524191a244a99f95fd4472f5402c2b"}, + {file = "aiohttp-3.14.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:672b9d65f42eb877f5c3f234a4547e4e1a226ca8c2eed879bb34670a0ce51192"}, + {file = "aiohttp-3.14.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:24ba13339fed9251d9b1a1bec8c7ab84c0d1675d79d33501e11f94f8b9a84e05"}, + {file = "aiohttp-3.14.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:94da27378da0610e341c4d30de29a191672683cc82b8f9556e8f7c7212a020fe"}, + {file = "aiohttp-3.14.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:52cdac9432d8b4a719f35094a818d95adcae0f0b4fe9b9b921909e0c87de9e7d"}, + {file = "aiohttp-3.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:672ac254412a24d0d0cf00a9e6c238877e4be5e5fa2d188832c1244f45f31966"}, + {file = "aiohttp-3.14.1-cp310-cp310-win32.whl", hash = "sha256:2fe3607e71acc6ebb0ec8e492a247bf7a291226192dc0084236dfc12478916f6"}, + {file = "aiohttp-3.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:30099eda75a53c32efb0920e9c33c195314d2cc1c680fbfd30894932ac5f27df"}, + {file = "aiohttp-3.14.1-cp310-cp310-win_arm64.whl", hash = "sha256:5a837f49d901f9e368651b676912bff1104ed8c1a83b280bcd7b29adccef5c9c"}, + {file = "aiohttp-3.14.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa00140699487bd435fde4342d85c94cb256b7cd3a5b9c3396c67f19922afda2"}, + {file = "aiohttp-3.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c1af67559445498b502030c35c59db59966f47041ca9de5b4e707f86bd10b5f"}, + {file = "aiohttp-3.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d44ec478e713ee7f29b439f7eb8dc2b9d4079e11ae114d2c2ac3d5daf30516c8"}, + {file = "aiohttp-3.14.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3b1a184a9a8f548a6b73f1e26b96b052193e4b3175ed7342aaf1151a1f00a04"}, + {file = "aiohttp-3.14.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5f2504bc0322437c9a1ff6d3333ca56c7477b727c995f036b976ae17b98372c8"}, + {file = "aiohttp-3.14.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73f05ea02013e02512c3bf42714f1208c57168c779cc6fe23516e4543089d0a6"}, + {file = "aiohttp-3.14.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:797457503c2d426bee06eef808d07b31ede30b65e054444e7de64cad0061b7af"}, + {file = "aiohttp-3.14.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b821a1f7dedf7e37450654e620038ac3b2e81e8fa6ea269337e97101978ec730"}, + {file = "aiohttp-3.14.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4cd96b5ba05d67ed0cf00b5b405c8cd99586d8e3481e8ee0a831057591af7621"}, + {file = "aiohttp-3.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d459b98a932296c6f0e94f87511a0b1b90a8a02c30a50e60a297619cd5a58ee"}, + {file = "aiohttp-3.14.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:764457a7be60825fb770a644852ff717bcbb5042f189f2bd16df61a81b3f6573"}, + {file = "aiohttp-3.14.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f7a16ef45b081454ef844502d87a848876c490c4cb5c650c230f6ec79ed2c1e7"}, + {file = "aiohttp-3.14.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2fbc3ed048b3475b9f0cbcb9978e9d2d3511acd91ead203af26ed9f0056004cf"}, + {file = "aiohttp-3.14.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bedb0cd073cc2dc035e30aeb99444389d3cd2113afe4ef9fcd23d439f5bade85"}, + {file = "aiohttp-3.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b6feea921016eb3d4e04d65fc4e9ca402d1a3801f562aef94989f54694917af3"}, + {file = "aiohttp-3.14.1-cp311-cp311-win32.whl", hash = "sha256:313701e488100074ce99850404ee36e741abf6330179fec908a1944ecf570126"}, + {file = "aiohttp-3.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:03ab4530fdcb3a543a122ba4b65ac9919da9fe9f78a03d328a6e38ff962f7aa5"}, + {file = "aiohttp-3.14.1-cp311-cp311-win_arm64.whl", hash = "sha256:486f7d16ed54c39c2cbd7ca71fd8ba2b8bb7860df65bd7b6ed640bab96a38a8b"}, + {file = "aiohttp-3.14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d35143e27778b4bb0fb189562d7f275bff79c62ab8e98459717c0ea617ff2480"}, + {file = "aiohttp-3.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bcfb80a2cc36fba2534e5e5b5264dc7ae6fcd9bf15256da3e53d2f499e6fa29d"}, + {file = "aiohttp-3.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27fd7c91e51729b4f7e1577865fa6d34c9adccbc39aabe9000285b48af9f0ec2"}, + {file = "aiohttp-3.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c567bf9eaf664280116a8688f63016e6b32db2505908e2bdaca1b6438142f2"}, + {file = "aiohttp-3.14.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f5e6ff2bdbb8f4cd3fbe41f99e25bbcd58e3bf9f13d3dd31a11e7917251cc77a"}, + {file = "aiohttp-3.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f73e01dc37122325caf079982621262f96d74823c179038a82fddfc50359264"}, + {file = "aiohttp-3.14.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb2c0c80d431c0d03f2c7dbf125150fedd4f0de17366a7ca33f7ccb822391842"}, + {file = "aiohttp-3.14.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e6fc1a85fa7194a1a7d19f44e8609180f4a8eb5fa4c7ed8b4355f080fad235c"}, + {file = "aiohttp-3.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:686b6c0d3911ec387b444ddf5dc62fb7f7c0a7d5186a7861626496a5ab4aff95"}, + {file = "aiohttp-3.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c6fa4dc7ad6f8109c70bb1499e589f76b0b792baf39f9b017eb92c8a81d0a199"}, + {file = "aiohttp-3.14.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:87a5eea1b2a5e21e1ebdbb33ad4165359189327e63fc4e4894693e7f821ac817"}, + {file = "aiohttp-3.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c1421eb01d4fd608d88cc8290211d177a58532b55ad94076fb349c5bf467f0a"}, + {file = "aiohttp-3.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:34b257ec41345c1e8f2df68fa908a7952f5de932723871eb633ecbbff396c9a4"}, + {file = "aiohttp-3.14.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:de538791a80e5d862addbc183f70f0158ac9b9bb872bb147f1fd2a683691e087"}, + {file = "aiohttp-3.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f71173be42d3241d428f760122febb748de0623f44308a6f120d0dd9ec572e3"}, + {file = "aiohttp-3.14.1-cp312-cp312-win32.whl", hash = "sha256:ec8dc383ee57ea3e883477dcca3f11b65d58199f1080acaf4cd6ad9a99698be4"}, + {file = "aiohttp-3.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2aa92c87868cd13674989f9ee83e5f9f7ea4237589b728048e1f0c8f6caa3271"}, + {file = "aiohttp-3.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:2c840c90759922cb5e6dda94596e079a30fb5a5ba548e7e0dc00574703940847"}, + {file = "aiohttp-3.14.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178"}, + {file = "aiohttp-3.14.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf"}, + {file = "aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd"}, + {file = "aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe"}, + {file = "aiohttp-3.14.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4"}, + {file = "aiohttp-3.14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876"}, + {file = "aiohttp-3.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da"}, + {file = "aiohttp-3.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6"}, + {file = "aiohttp-3.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96"}, + {file = "aiohttp-3.14.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f"}, + {file = "aiohttp-3.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296"}, + {file = "aiohttp-3.14.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa"}, + {file = "aiohttp-3.14.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451"}, + {file = "aiohttp-3.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c"}, + {file = "aiohttp-3.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca"}, + {file = "aiohttp-3.14.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09"}, + {file = "aiohttp-3.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397"}, + {file = "aiohttp-3.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080"}, + {file = "aiohttp-3.14.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345"}, + {file = "aiohttp-3.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588"}, + {file = "aiohttp-3.14.1-cp313-cp313-win32.whl", hash = "sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780"}, + {file = "aiohttp-3.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a"}, + {file = "aiohttp-3.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8"}, + {file = "aiohttp-3.14.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15"}, + {file = "aiohttp-3.14.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8"}, + {file = "aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3"}, + {file = "aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba"}, + {file = "aiohttp-3.14.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397"}, + {file = "aiohttp-3.14.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448"}, + {file = "aiohttp-3.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004"}, + {file = "aiohttp-3.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983"}, + {file = "aiohttp-3.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe"}, + {file = "aiohttp-3.14.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333"}, + {file = "aiohttp-3.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0"}, + {file = "aiohttp-3.14.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a"}, + {file = "aiohttp-3.14.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602"}, + {file = "aiohttp-3.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca"}, + {file = "aiohttp-3.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35"}, + {file = "aiohttp-3.14.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844"}, + {file = "aiohttp-3.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496"}, + {file = "aiohttp-3.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5"}, + {file = "aiohttp-3.14.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95"}, + {file = "aiohttp-3.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444"}, + {file = "aiohttp-3.14.1-cp314-cp314-win32.whl", hash = "sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0"}, + {file = "aiohttp-3.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719"}, + {file = "aiohttp-3.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec"}, + {file = "aiohttp-3.14.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2"}, + {file = "aiohttp-3.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340"}, + {file = "aiohttp-3.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d"}, + {file = "aiohttp-3.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94"}, + {file = "aiohttp-3.14.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc"}, + {file = "aiohttp-3.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251"}, + {file = "aiohttp-3.14.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1"}, + {file = "aiohttp-3.14.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3"}, + {file = "aiohttp-3.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c"}, + {file = "aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203"}, + {file = "aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1"}, + {file = "aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665"}, + {file = "aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1"}, + {file = "aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d"}, + {file = "aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c"}, + {file = "aiohttp-3.14.1-cp314-cp314t-win32.whl", hash = "sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365"}, + {file = "aiohttp-3.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9"}, + {file = "aiohttp-3.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6"}, + {file = "aiohttp-3.14.1.tar.gz", hash = "sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035"}, +] + +[package.dependencies] +aiohappyeyeballs = ">=2.5.0" +aiosignal = ">=1.4.0" +async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""} +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +propcache = ">=0.2.0" +typing_extensions = {version = ">=4.4", markers = "python_version < \"3.13\""} +yarl = ">=1.17.0,<2.0" + +[package.extras] +speedups = ["Brotli (>=1.2) ; platform_python_implementation == \"CPython\" and sys_platform != \"android\" and sys_platform != \"ios\"", "aiodns (>=3.3.0) ; sys_platform != \"android\" and sys_platform != \"ios\"", "backports.zstd ; platform_python_implementation == \"CPython\" and python_version < \"3.14\" and sys_platform != \"android\" and sys_platform != \"ios\"", "brotlicffi (>=1.2) ; platform_python_implementation != \"CPython\""] + +[[package]] +name = "aiosignal" +version = "1.4.0" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"aiohttp\"" +files = [ + {file = "aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}, + {file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" +typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} [[package]] name = "annotated-types" @@ -6,45 +180,81 @@ version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] -[package.dependencies] -typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.9\""} - [[package]] name = "anyio" -version = "4.5.2" -description = "High level compatibility layer for multiple asynchronous event loop implementations" +version = "4.14.0" +description = "High-level concurrency and networking framework on top of asyncio or Trio" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" +groups = ["main"] files = [ - {file = "anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f"}, - {file = "anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b"}, + {file = "anyio-4.14.0-py3-none-any.whl", hash = "sha256:dd9b7a2a9799ed6552fde617b2c5df02b7fdd7d88392fc48101e51bae46164d9"}, + {file = "anyio-4.14.0.tar.gz", hash = "sha256:b47c1f9ccf73e67021df785332508f99379c68fa7d0684e8e3492cb1d4b23f89"}, ] [package.dependencies] exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" -sniffio = ">=1.1" -typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] -doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] -trio = ["trio (>=0.26.1)"] +trio = ["trio (>=0.32.0)"] + +[[package]] +name = "async-timeout" +version = "5.0.1" +description = "Timeout context manager for asyncio programs" +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version == \"3.10\" and extra == \"aiohttp\"" +files = [ + {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, + {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, +] + +[[package]] +name = "attrs" +version = "26.1.0" +description = "Classes Without Boilerplate" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"aiohttp\"" +files = [ + {file = "attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309"}, + {file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"}, +] + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +description = "Backport of asyncio.Runner, a context manager that controls event loop life cycle." +optional = false +python-versions = "<3.11,>=3.8" +groups = ["dev"] +markers = "python_version == \"3.10\"" +files = [ + {file = "backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5"}, + {file = "backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162"}, +] [[package]] name = "certifi" -version = "2026.4.22" +version = "2026.6.17" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ - {file = "certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a"}, - {file = "certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580"}, + {file = "certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db"}, + {file = "certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432"}, ] [[package]] @@ -53,6 +263,8 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -64,6 +276,8 @@ version = "1.3.1" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] +markers = "python_version == \"3.10\"" files = [ {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, @@ -75,12 +289,169 @@ typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "execnet" +version = "2.1.2" +description = "execnet: rapid multi-Python deployment" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec"}, + {file = "execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd"}, +] + +[package.extras] +testing = ["hatch", "pre-commit", "pytest", "tox"] + +[[package]] +name = "frozenlist" +version = "1.8.0" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"aiohttp\"" +files = [ + {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011"}, + {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565"}, + {file = "frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7"}, + {file = "frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a"}, + {file = "frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6"}, + {file = "frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967"}, + {file = "frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25"}, + {file = "frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b"}, + {file = "frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa"}, + {file = "frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf"}, + {file = "frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746"}, + {file = "frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed"}, + {file = "frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496"}, + {file = "frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231"}, + {file = "frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7"}, + {file = "frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806"}, + {file = "frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0"}, + {file = "frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda"}, + {file = "frozenlist-1.8.0-cp39-cp39-win32.whl", hash = "sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087"}, + {file = "frozenlist-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a"}, + {file = "frozenlist-1.8.0-cp39-cp39-win_arm64.whl", hash = "sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103"}, + {file = "frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d"}, + {file = "frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad"}, +] + [[package]] name = "h11" version = "0.16.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, @@ -92,6 +463,7 @@ version = "1.0.9" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, @@ -113,6 +485,7 @@ version = "0.28.1" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, @@ -125,21 +498,39 @@ httpcore = "==1.*" idna = "*" [package.extras] -brotli = ["brotli", "brotlicffi"] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "httpx-aiohttp" +version = "0.1.8" +description = "Aiohttp transport for HTTPX" +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"aiohttp\"" +files = [ + {file = "httpx_aiohttp-0.1.8-py3-none-any.whl", hash = "sha256:b7bd958d1331f3759a38a0ba22ad29832cb63ca69498c17735228055bf78fa7e"}, + {file = "httpx_aiohttp-0.1.8.tar.gz", hash = "sha256:756c5e74cdb568c3248ba63fe82bfe8bbe64b928728720f7eaac64b3cf46f308"}, +] + +[package.dependencies] +aiohttp = ">=3.10.0,<4" +httpx = ">=0.27.0" + [[package]] name = "idna" -version = "3.13" +version = "3.18" description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3"}, - {file = "idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242"}, + {file = "idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2"}, + {file = "idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848"}, ] [package.extras] @@ -147,59 +538,228 @@ all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] [[package]] name = "iniconfig" -version = "2.1.0" +version = "2.3.0" description = "brain-dead simple config-ini parsing" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" +groups = ["dev"] files = [ - {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, - {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, ] +[[package]] +name = "multidict" +version = "6.7.1" +description = "multidict implementation" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"aiohttp\"" +files = [ + {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5"}, + {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8"}, + {file = "multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505"}, + {file = "multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122"}, + {file = "multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df"}, + {file = "multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa"}, + {file = "multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a"}, + {file = "multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b"}, + {file = "multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba"}, + {file = "multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511"}, + {file = "multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19"}, + {file = "multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33"}, + {file = "multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3"}, + {file = "multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5"}, + {file = "multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108"}, + {file = "multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32"}, + {file = "multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8"}, + {file = "multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b"}, + {file = "multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d"}, + {file = "multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f"}, + {file = "multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2"}, + {file = "multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7"}, + {file = "multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5"}, + {file = "multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5"}, + {file = "multidict-6.7.1-cp39-cp39-win32.whl", hash = "sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0"}, + {file = "multidict-6.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4"}, + {file = "multidict-6.7.1-cp39-cp39-win_arm64.whl", hash = "sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9"}, + {file = "multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56"}, + {file = "multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} + [[package]] name = "mypy" -version = "1.0.1" +version = "1.13.0" description = "Optional static typing for Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "mypy-1.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:71a808334d3f41ef011faa5a5cd8153606df5fc0b56de5b2e89566c8093a0c9a"}, - {file = "mypy-1.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:920169f0184215eef19294fa86ea49ffd4635dedfdea2b57e45cb4ee85d5ccaf"}, - {file = "mypy-1.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27a0f74a298769d9fdc8498fcb4f2beb86f0564bcdb1a37b58cbbe78e55cf8c0"}, - {file = "mypy-1.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:65b122a993d9c81ea0bfde7689b3365318a88bde952e4dfa1b3a8b4ac05d168b"}, - {file = "mypy-1.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:5deb252fd42a77add936b463033a59b8e48eb2eaec2976d76b6878d031933fe4"}, - {file = "mypy-1.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2013226d17f20468f34feddd6aae4635a55f79626549099354ce641bc7d40262"}, - {file = "mypy-1.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:48525aec92b47baed9b3380371ab8ab6e63a5aab317347dfe9e55e02aaad22e8"}, - {file = "mypy-1.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c96b8a0c019fe29040d520d9257d8c8f122a7343a8307bf8d6d4a43f5c5bfcc8"}, - {file = "mypy-1.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:448de661536d270ce04f2d7dddaa49b2fdba6e3bd8a83212164d4174ff43aa65"}, - {file = "mypy-1.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:d42a98e76070a365a1d1c220fcac8aa4ada12ae0db679cb4d910fabefc88b994"}, - {file = "mypy-1.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e64f48c6176e243ad015e995de05af7f22bbe370dbb5b32bd6988438ec873919"}, - {file = "mypy-1.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fdd63e4f50e3538617887e9aee91855368d9fc1dea30da743837b0df7373bc4"}, - {file = "mypy-1.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dbeb24514c4acbc78d205f85dd0e800f34062efcc1f4a4857c57e4b4b8712bff"}, - {file = "mypy-1.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a2948c40a7dd46c1c33765718936669dc1f628f134013b02ff5ac6c7ef6942bf"}, - {file = "mypy-1.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5bc8d6bd3b274dd3846597855d96d38d947aedba18776aa998a8d46fabdaed76"}, - {file = "mypy-1.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:17455cda53eeee0a4adb6371a21dd3dbf465897de82843751cf822605d152c8c"}, - {file = "mypy-1.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e831662208055b006eef68392a768ff83596035ffd6d846786578ba1714ba8f6"}, - {file = "mypy-1.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e60d0b09f62ae97a94605c3f73fd952395286cf3e3b9e7b97f60b01ddfbbda88"}, - {file = "mypy-1.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:0af4f0e20706aadf4e6f8f8dc5ab739089146b83fd53cb4a7e0e850ef3de0bb6"}, - {file = "mypy-1.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:24189f23dc66f83b839bd1cce2dfc356020dfc9a8bae03978477b15be61b062e"}, - {file = "mypy-1.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:93a85495fb13dc484251b4c1fd7a5ac370cd0d812bbfc3b39c1bafefe95275d5"}, - {file = "mypy-1.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f546ac34093c6ce33f6278f7c88f0f147a4849386d3bf3ae193702f4fe31407"}, - {file = "mypy-1.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c6c2ccb7af7154673c591189c3687b013122c5a891bb5651eca3db8e6c6c55bd"}, - {file = "mypy-1.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:15b5a824b58c7c822c51bc66308e759243c32631896743f030daf449fe3677f3"}, - {file = "mypy-1.0.1-py3-none-any.whl", hash = "sha256:eda5c8b9949ed411ff752b9a01adda31afe7eae1e53e946dbdf9db23865e66c4"}, - {file = "mypy-1.0.1.tar.gz", hash = "sha256:28cea5a6392bb43d266782983b5a4216c25544cd7d80be681a155ddcdafd152d"}, + {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, + {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, + {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"}, + {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, + {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, + {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"}, + {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, + {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, + {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"}, + {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, + {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, + {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, + {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, + {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"}, + {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"}, + {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"}, + {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"}, + {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"}, + {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"}, + {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"}, + {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, + {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, ] [package.dependencies] -mypy-extensions = ">=0.4.3" +mypy-extensions = ">=1.0.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=3.10" +typing-extensions = ">=4.6.0" [package.extras] dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] install-types = ["pip"] -python2 = ["typed-ast (>=1.4.0,<2)"] +mypyc = ["setuptools (>=50)"] reports = ["lxml"] [[package]] @@ -208,6 +768,7 @@ version = "1.1.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, @@ -219,6 +780,7 @@ version = "26.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"}, {file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"}, @@ -226,197 +788,395 @@ files = [ [[package]] name = "pluggy" -version = "1.5.0" +version = "1.6.0" description = "plugin and hook calling mechanisms for python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["dev"] files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, ] [package.extras] dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "propcache" +version = "0.5.2" +description = "Accelerated property cache" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"aiohttp\"" +files = [ + {file = "propcache-0.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a81be28596d6559f6131ef33e10200de6e17643b3c74ce03f9eb103be6ae8b"}, + {file = "propcache-0.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29cbaac5ea0212663e6845e04b5e188d5a6ae6dd919810ac835bf1d3b42c3f4c"}, + {file = "propcache-0.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6bf3be92233808fcd338eba0fb4d0b59ec5772af4f4ecfcec450d1bfc0f8b5eb"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f8ea531c794b9d6274acd4e8d2c2ebcac590a4361d27482edd3010b79f1325e"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:decfca4c79dd53ebab484b00cc4b6717d8c369f86e74aa4ca395a64ac651495e"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4621064bbf28fa77ff64dd5d94367c04684c67d3a5bf1dff25f0cd0d98a38f3b"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b96db7141a592cbc968daf1feea83a118e6ab378af4abbc72b248c895414c22d"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ca071adabaab6e9219924bbe00af821f1ee7de113a9eca1cdc292de3d120f4d"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e4294d04a94dcab1b3bccd8b66d962dcad411a1d19414b2a41d1445f1de32ad0"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a0e399a2eccb91ed18721f86aa85757727400b6865c89e88934781deb9c8498b"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:823581fd5cb08b12a48bfa11fe962a7916766b6170c17b028fbdf762b85eb9bf"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:949c91d1a990cf3b2e8188dfcfb25005e0b834a06c63fa4ef9f360878ce21ecf"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:cc1177027eda740fdb152706bd215a3f124e3eea15afc39f2cb9fe351b50619e"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b05d643f944a8c3c4bd86d65ffd87bf3264b617f87791940302bc474d2ff5274"}, + {file = "propcache-0.5.2-cp310-cp310-win32.whl", hash = "sha256:8114f28879e0904748e831c3a7774261bd9e75f49be089f389a76f959dcd13fe"}, + {file = "propcache-0.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:5fcb98e7598b1ee0addab320d90f65b530297a867dbfe9de52ea838077e16e3d"}, + {file = "propcache-0.5.2-cp310-cp310-win_arm64.whl", hash = "sha256:04dc2390d9edbbaef7461f33322555976ffddf0b650a038649d026358714e6c5"}, + {file = "propcache-0.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78"}, + {file = "propcache-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959"}, + {file = "propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f"}, + {file = "propcache-0.5.2-cp311-cp311-win32.whl", hash = "sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0"}, + {file = "propcache-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82"}, + {file = "propcache-0.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab"}, + {file = "propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba"}, + {file = "propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a"}, + {file = "propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a"}, + {file = "propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031"}, + {file = "propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42"}, + {file = "propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84"}, + {file = "propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a"}, + {file = "propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117"}, + {file = "propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4"}, + {file = "propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d"}, + {file = "propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757"}, + {file = "propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f"}, + {file = "propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d"}, + {file = "propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa"}, + {file = "propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568"}, + {file = "propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191"}, + {file = "propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7"}, + {file = "propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96"}, + {file = "propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999"}, + {file = "propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e"}, + {file = "propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4"}, + {file = "propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0"}, + {file = "propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c"}, + {file = "propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0"}, + {file = "propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb"}, + {file = "propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078"}, + {file = "propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2"}, + {file = "propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821"}, + {file = "propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370"}, + {file = "propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6"}, + {file = "propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe"}, + {file = "propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427"}, +] [[package]] name = "pydantic" -version = "2.10.6" +version = "2.13.4" description = "Data validation using Python type hints" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"}, - {file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"}, + {file = "pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba"}, + {file = "pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6"}, ] [package.dependencies] annotated-types = ">=0.6.0" -pydantic-core = "2.27.2" -typing-extensions = ">=4.12.2" +pydantic-core = "2.46.4" +typing-extensions = ">=4.14.1" +typing-inspection = ">=0.4.2" [package.extras] email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] [[package]] name = "pydantic-core" -version = "2.27.2" +version = "2.46.4" description = "Core functionality for Pydantic validation and serialization" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, - {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"}, - {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"}, - {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"}, - {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"}, - {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"}, - {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"}, - {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"}, - {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"}, - {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"}, - {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"}, - {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"}, - {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"}, - {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"}, - {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"}, - {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"}, - {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"}, - {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"}, + {file = "pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4"}, + {file = "pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d"}, + {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4"}, + {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f"}, + {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39"}, + {file = "pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d"}, + {file = "pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf"}, + {file = "pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594"}, + {file = "pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3"}, + {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848"}, + {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3"}, + {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109"}, + {file = "pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda"}, + {file = "pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33"}, + {file = "pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d"}, + {file = "pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2"}, + {file = "pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b"}, + {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458"}, + {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b"}, + {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c"}, + {file = "pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894"}, + {file = "pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89"}, + {file = "pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a"}, + {file = "pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008"}, + {file = "pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e"}, + {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd"}, + {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be"}, + {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d"}, + {file = "pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb"}, + {file = "pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292"}, + {file = "pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d"}, + {file = "pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb"}, + {file = "pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb"}, + {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898"}, + {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e"}, + {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519"}, + {file = "pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4"}, + {file = "pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac"}, + {file = "pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596"}, + {file = "pydantic_core-2.46.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae"}, + {file = "pydantic_core-2.46.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6"}, + {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9"}, + {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29"}, + {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9"}, + {file = "pydantic_core-2.46.4-cp39-cp39-win32.whl", hash = "sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1"}, + {file = "pydantic_core-2.46.4-cp39-cp39-win_amd64.whl", hash = "sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983"}, + {file = "pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1"}, ] [package.dependencies] -typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +typing-extensions = ">=4.14.1" + +[[package]] +name = "pygments" +version = "2.20.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, + {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pytest" -version = "7.4.4" +version = "9.1.1" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" +groups = ["dev"] files = [ - {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, - {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, + {file = "pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c"}, + {file = "pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313"}, ] [package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} +iniconfig = ">=1.0.1" +packaging = ">=22" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-asyncio" -version = "0.23.8" +version = "1.4.0" description = "Pytest support for asyncio" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" +groups = ["dev"] files = [ - {file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"}, - {file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"}, + {file = "pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1"}, + {file = "pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42"}, ] [package.dependencies] -pytest = ">=7.0.0,<9" +backports-asyncio-runner = {version = ">=1.1,<2", markers = "python_version < \"3.11\""} +pytest = ">=8.4,<10" +typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""} [package.extras] -docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)", "sphinx-tabs (>=3.5)"] testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] +[[package]] +name = "pytest-xdist" +version = "3.8.0" +description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88"}, + {file = "pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1"}, +] + +[package.dependencies] +execnet = ">=2.1" +pytest = ">=7.0.0" + +[package.extras] +psutil = ["psutil (>=3.0)"] +setproctitle = ["setproctitle"] +testing = ["filelock"] + [[package]] name = "python-dateutil" version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["dev"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -427,29 +1187,30 @@ six = ">=1.5" [[package]] name = "ruff" -version = "0.5.7" +version = "0.11.5" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ - {file = "ruff-0.5.7-py3-none-linux_armv6l.whl", hash = "sha256:548992d342fc404ee2e15a242cdbea4f8e39a52f2e7752d0e4cbe88d2d2f416a"}, - {file = "ruff-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be"}, - {file = "ruff-0.5.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf3d86a1fdac1aec8a3417a63587d93f906c678bb9ed0b796da7b59c1114a1e"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a01c34400097b06cf8a6e61b35d6d456d5bd1ae6961542de18ec81eaf33b4cb8"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcc8054f1a717e2213500edaddcf1dbb0abad40d98e1bd9d0ad364f75c763eea"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f70284e73f36558ef51602254451e50dd6cc479f8b6f8413a95fcb5db4a55fc"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a78ad870ae3c460394fc95437d43deb5c04b5c29297815a2a1de028903f19692"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ccd078c66a8e419475174bfe60a69adb36ce04f8d4e91b006f1329d5cd44bcf"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e31c9bad4ebf8fdb77b59cae75814440731060a09a0e0077d559a556453acbb"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d796327eed8e168164346b769dd9a27a70e0298d667b4ecee6877ce8095ec8e"}, - {file = "ruff-0.5.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a09ea2c3f7778cc635e7f6edf57d566a8ee8f485f3c4454db7771efb692c499"}, - {file = "ruff-0.5.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a36d8dcf55b3a3bc353270d544fb170d75d2dff41eba5df57b4e0b67a95bb64e"}, - {file = "ruff-0.5.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9369c218f789eefbd1b8d82a8cf25017b523ac47d96b2f531eba73770971c9e5"}, - {file = "ruff-0.5.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b88ca3db7eb377eb24fb7c82840546fb7acef75af4a74bd36e9ceb37a890257e"}, - {file = "ruff-0.5.7-py3-none-win32.whl", hash = "sha256:33d61fc0e902198a3e55719f4be6b375b28f860b09c281e4bdbf783c0566576a"}, - {file = "ruff-0.5.7-py3-none-win_amd64.whl", hash = "sha256:083bbcbe6fadb93cd86709037acc510f86eed5a314203079df174c40bbbca6b3"}, - {file = "ruff-0.5.7-py3-none-win_arm64.whl", hash = "sha256:2dca26154ff9571995107221d0aeaad0e75a77b5a682d6236cf89a58c70b76f4"}, - {file = "ruff-0.5.7.tar.gz", hash = "sha256:8dfc0a458797f5d9fb622dd0efc52d796f23f0a1493a9527f4e49a550ae9a7e5"}, + {file = "ruff-0.11.5-py3-none-linux_armv6l.whl", hash = "sha256:2561294e108eb648e50f210671cc56aee590fb6167b594144401532138c66c7b"}, + {file = "ruff-0.11.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac12884b9e005c12d0bd121f56ccf8033e1614f736f766c118ad60780882a077"}, + {file = "ruff-0.11.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4bfd80a6ec559a5eeb96c33f832418bf0fb96752de0539905cf7b0cc1d31d779"}, + {file = "ruff-0.11.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0947c0a1afa75dcb5db4b34b070ec2bccee869d40e6cc8ab25aca11a7d527794"}, + {file = "ruff-0.11.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad871ff74b5ec9caa66cb725b85d4ef89b53f8170f47c3406e32ef040400b038"}, + {file = "ruff-0.11.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6cf918390cfe46d240732d4d72fa6e18e528ca1f60e318a10835cf2fa3dc19f"}, + {file = "ruff-0.11.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56145ee1478582f61c08f21076dc59153310d606ad663acc00ea3ab5b2125f82"}, + {file = "ruff-0.11.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5f66f8f1e8c9fc594cbd66fbc5f246a8d91f916cb9667e80208663ec3728304"}, + {file = "ruff-0.11.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80b4df4d335a80315ab9afc81ed1cff62be112bd165e162b5eed8ac55bfc8470"}, + {file = "ruff-0.11.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3068befab73620b8a0cc2431bd46b3cd619bc17d6f7695a3e1bb166b652c382a"}, + {file = "ruff-0.11.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5da2e710a9641828e09aa98b92c9ebbc60518fdf3921241326ca3e8f8e55b8b"}, + {file = "ruff-0.11.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ef39f19cb8ec98cbc762344921e216f3857a06c47412030374fffd413fb8fd3a"}, + {file = "ruff-0.11.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b2a7cedf47244f431fd11aa5a7e2806dda2e0c365873bda7834e8f7d785ae159"}, + {file = "ruff-0.11.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:81be52e7519f3d1a0beadcf8e974715b2dfc808ae8ec729ecfc79bddf8dbb783"}, + {file = "ruff-0.11.5-py3-none-win32.whl", hash = "sha256:e268da7b40f56e3eca571508a7e567e794f9bfcc0f412c4b607931d3af9c4afe"}, + {file = "ruff-0.11.5-py3-none-win_amd64.whl", hash = "sha256:6c6dc38af3cfe2863213ea25b6dc616d679205732dc0fb673356c2d69608f800"}, + {file = "ruff-0.11.5-py3-none-win_arm64.whl", hash = "sha256:67e241b4314f4eacf14a601d586026a962f4002a475aa702c69980a38087aa4e"}, + {file = "ruff-0.11.5.tar.gz", hash = "sha256:cae2e2439cb88853e421901ec040a758960b576126dab520fa08e9de431d1bef"}, ] [[package]] @@ -458,28 +1219,20 @@ version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["dev"] files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] -[[package]] -name = "sniffio" -version = "1.3.1" -description = "Sniff out which async library your code is running under" -optional = false -python-versions = ">=3.7" -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - [[package]] name = "tomli" version = "2.4.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version == \"3.10\"" files = [ {file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"}, {file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"}, @@ -532,27 +1285,185 @@ files = [ [[package]] name = "types-python-dateutil" -version = "2.9.0.20241206" +version = "2.9.0.20260518" description = "Typing stubs for python-dateutil" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" +groups = ["dev"] files = [ - {file = "types_python_dateutil-2.9.0.20241206-py3-none-any.whl", hash = "sha256:e248a4bc70a486d3e3ec84d0dc30eec3a5f979d6e7ee4123ae043eedbb987f53"}, - {file = "types_python_dateutil-2.9.0.20241206.tar.gz", hash = "sha256:18f493414c26ffba692a72369fea7a154c502646301ebfe3d56a04b3767284cb"}, + {file = "types_python_dateutil-2.9.0.20260518-py3-none-any.whl", hash = "sha256:d6a9c5bd0de61460c8fdef8ab2b400f956a1a1075cce08d4e2b4434e478c50b8"}, + {file = "types_python_dateutil-2.9.0.20260518.tar.gz", hash = "sha256:51f02dc03b61c7f6a07df45797d4dfe8a1aa47f0b7db9ad89f6fd3a1a70e1b51"}, ] [[package]] name = "typing-extensions" -version = "4.13.2" -description = "Backported and Experimental Type Hints for Python 3.8+" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main", "dev"] files = [ - {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, - {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ] +[[package]] +name = "typing-inspection" +version = "0.4.2" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, + {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + +[[package]] +name = "urllib3" +version = "2.7.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"}, + {file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"}, +] + +[package.extras] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] + +[[package]] +name = "yarl" +version = "1.24.2" +description = "Yet another URL library" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"aiohttp\"" +files = [ + {file = "yarl-1.24.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5249a113065c2b7a958bc699759e359cd61cfc81e3069662208f48f191b7ed12"}, + {file = "yarl-1.24.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7f4425fa244fbf530b006d0c5f79ce920114cfff5b4f5f6056e669f8e160fdc0"}, + {file = "yarl-1.24.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15c0b5e49d3c44e2a0b93e6a49476c5edad0a7686b92c395765a7ea775572a75"}, + {file = "yarl-1.24.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:246d32a53a947c8f0189f5d699cbd4c7036de45d9359e13ba238d1239678c727"}, + {file = "yarl-1.24.2-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:64480fb3e4d4ed9ed71c48a91a477384fc342a50ca30071d2f8a88d51d9c9413"}, + {file = "yarl-1.24.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:349de4701dc3760b6e876628423a8f147ef4f5599d10aba1e10702075d424ed9"}, + {file = "yarl-1.24.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d162677af8d5d3d6ebab8394b021f4d041ac107a4b705873148a77a49dc9e1b2"}, + {file = "yarl-1.24.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5f5c6ec23a9043f2d139cc072f53dd23168d202a334b9b2fda8de4c3e890d90"}, + {file = "yarl-1.24.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:60de6742447fbbf697f16f070b8a443f1b5fe6ca3826fbef9fe70ecd5328e643"}, + {file = "yarl-1.24.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:acf93187c3710e422368eb768aee98db551ec7c85adc250207a95c16548ab7ac"}, + {file = "yarl-1.24.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f4b0352fd41fd34b6651934606268816afd6914d09626f9bcbbf018edb0afb3f"}, + {file = "yarl-1.24.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6b208bb939099b4b297438da4e9b25357f0b1c791888669b963e45b203ea9f36"}, + {file = "yarl-1.24.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4b85b8825e631295ff4bc8943f7471d54c533a9360bbe15ebb38e018b555bb8a"}, + {file = "yarl-1.24.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e26acf20c26cb4fefc631fdb75aca2a6b8fa8b7b5d7f204fb6a8f1e63c706f53"}, + {file = "yarl-1.24.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:819ca24f8eafcfb683c1bd5f44f2f488cea1274eb8944731ffd2e1f10f619342"}, + {file = "yarl-1.24.2-cp310-cp310-win_amd64.whl", hash = "sha256:5cb0f995a901c36be096ccbf4c673591c2faabbe96279598ffaec8c030f85bf4"}, + {file = "yarl-1.24.2-cp310-cp310-win_arm64.whl", hash = "sha256:f408eace7e22a68b467a0562e0d27d322f91fe3eaaa6f466b962c6cfaea9fa39"}, + {file = "yarl-1.24.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:36348bebb147b83818b9d7e673ea4debc75970afc6ffdc7e3975ad05ce5a58c1"}, + {file = "yarl-1.24.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a97e42c8a2233f2f279ecadd9e4a037bcb5d813b78435e8eedd4db5a9e9708c"}, + {file = "yarl-1.24.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8d027d56f1035e339d1001ac33eceab5b2ec8e42e449787bb75e289fb9a5cd1d"}, + {file = "yarl-1.24.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a6377060e7927187a42b7eb202090cbe2b34933a4eeaf90e3bd9e33432e5cae"}, + {file = "yarl-1.24.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:17076578bce0049a5ce57d14ad1bded391b68a3b213e9b81b0097b090244999a"}, + {file = "yarl-1.24.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:50713f1d4d6be6375bb178bb43d140ee1acb8abe589cd723320b7925a275be1e"}, + {file = "yarl-1.24.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:34263e2fa8fb5bb63a0d97706cda38edbad62fddb58c7f12d6acbc092812aa50"}, + {file = "yarl-1.24.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49016d82f032b1bd1e10b01078a7d29ae71bf468eeae0ea22df8bab691e60003"}, + {file = "yarl-1.24.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3f6d2c216318f8f32038ca3f72501ba08536f0fd18a36e858836b121b2deed9f"}, + {file = "yarl-1.24.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:08d3a33218e0c64393e7610284e770409a9c31c429b078bcb24096ed0a783b8f"}, + {file = "yarl-1.24.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5d699376c4ca3cba49bbfae3a05b5b70ded572937171ce1e0b8d87118e2ba294"}, + {file = "yarl-1.24.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a1cab588b4fa14bea2e55ebea27478adfb05372f47573738e1acc4a36c0b05d2"}, + {file = "yarl-1.24.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ec87ccc31bd21db7ad009d8572c127c1000f268517618a4cc09adba3c2a7f21c"}, + {file = "yarl-1.24.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d1dd47a22843b212baa8d74f37796815d43bd046b42a0f41e9da433386c3136b"}, + {file = "yarl-1.24.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7b54b9c67c2b06bd7b9a77253d242124b9c95d2c02def5a1144001ee547dd9d5"}, + {file = "yarl-1.24.2-cp311-cp311-win_amd64.whl", hash = "sha256:f8fdbcff8b2c7c9284e60c196f693588598ddcee31e11c18e14949ce44519d45"}, + {file = "yarl-1.24.2-cp311-cp311-win_arm64.whl", hash = "sha256:b32c37a7a337e90822c45797bf3d79d60875cfcccd3ecc80e9f453d87026c122"}, + {file = "yarl-1.24.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b975866c184564c827e0877380f0dae57dcca7e52782128381b72feff6dfceb8"}, + {file = "yarl-1.24.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3b075301a2836a0e297b1b658cb6d6135df535d62efefdd60366bd589c2c82f2"}, + {file = "yarl-1.24.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ae44649b00947634ab0dab2a374a638f52923a6e67083f2c156cd5cbd1a881d"}, + {file = "yarl-1.24.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:507cc19f0b45454e2d6dcd62ff7d062b9f77a2812404e62dbdaec05b50faa035"}, + {file = "yarl-1.24.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4c17bad5a530912d2111825d3f05e89bab2dd376aaa8cbc77e449e6db63e576"}, + {file = "yarl-1.24.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5f0cbb112838a4a293985b6ed73948a547dadcc1ba6d2089938e7abdedceef8"}, + {file = "yarl-1.24.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ec8356b8a6afcf81fc7aeeef13b1ff7a49dec00f313394bbb9e83830d32ccd7"}, + {file = "yarl-1.24.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e7ebcdef69dec6c6451e616f32b622a6d4a2e92b445c992f7c8e5274a6bbc4c"}, + {file = "yarl-1.24.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:47a55d6cf6db2f401017a9e96e5288844e5051911fb4e0c8311a3980f5e59a7d"}, + {file = "yarl-1.24.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3065657c80a2321225e804048597ad55658a7e76b32d6f5ee4074d04c50401db"}, + {file = "yarl-1.24.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cb84b80d88e19ede158619b80813968713d8d008b0e2497a576e6a0557d50712"}, + {file = "yarl-1.24.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:990de4f680b1c217e77ff0d6aa0029f9eb79889c11fb3e9a3942c7eba29c1996"}, + {file = "yarl-1.24.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:abb8ec0323b80161e3802da3150ef660b41d0e9be2048b76a363d93eee992c2b"}, + {file = "yarl-1.24.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e7977781f83638a4c73e0f88425563d70173e0dfd90ac006a45c65036293ee3c"}, + {file = "yarl-1.24.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e30dd55825dc554ec5b66a94953b8eda8745926514c5089dfcacecb9c99b5bd1"}, + {file = "yarl-1.24.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dafe10c12ddd4d120d528c4b5599c953bd7b12845347d507b95451195bb6cad"}, + {file = "yarl-1.24.2-cp312-cp312-win_arm64.whl", hash = "sha256:044a09d8401fcf8681977faef6d286b8ade1e2d2e9dceda175d1cfa5ca496f30"}, + {file = "yarl-1.24.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:491ac9141decf49ee8030199e1ee251cdff0e131f25678817ff6aa5f837a3536"}, + {file = "yarl-1.24.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e89418f65eda18f99030386305bd44d7d504e328a7945db1ead514fbe03a0607"}, + {file = "yarl-1.24.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cdfcce633b4a4bb8281913c57fcafd4b5933fbc19111a5e3930bbd299d6102f1"}, + {file = "yarl-1.24.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:863297ddede92ee49024e9a9b11ecb59f310ca85b60d8537f56bed9bbb5b1986"}, + {file = "yarl-1.24.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:374423f70754a2c96942ede36a29d37dc6b0cb8f92f8d009ddf3ed78d3da5488"}, + {file = "yarl-1.24.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33a29b5d00ccbf3219bb3e351d7875739c19481e030779f48cc46a7a71681a9b"}, + {file = "yarl-1.24.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a9532c57211730c515341af11fef6e9b61d157487272a096d0c04da445642592"}, + {file = "yarl-1.24.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91e72cf093fd833483a97ee648e0c053c7c629f51ff4a0e7edd84f806b0c5617"}, + {file = "yarl-1.24.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b3177bc0a768ef3bacceb4f272632990b7bea352f1b2f1eee9d6d6ff16516f92"}, + {file = "yarl-1.24.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e196952aacaf3b232e265ff02980b64d483dc0972bd49bcb061171ff22ac203a"}, + {file = "yarl-1.24.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:204e7a61ce99919c0de1bf904ab5d7aa188a129ea8f690a8f76cfb6e2844dc44"}, + {file = "yarl-1.24.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b156914620f0b9d78dc1adb3751141daee561cfec796088abb89ed49d220f1a"}, + {file = "yarl-1.24.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8372a2b976cf70654b2be6619ab6068acabb35f724c0fda7b277fbf53d66a5cf"}, + {file = "yarl-1.24.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f9a1e9b622ca284143aab5d885848686dcd85453bb1ca9abcdb7503e64dc0056"}, + {file = "yarl-1.24.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:810e19b685c8c3c5862f6a38160a1f4e4c0916c9390024ec347b6157a45a0992"}, + {file = "yarl-1.24.2-cp313-cp313-win_amd64.whl", hash = "sha256:7d37fb7c38f2b6edab0f845c4f85148d4c44204f52bc127021bd2bc9fdbf1656"}, + {file = "yarl-1.24.2-cp313-cp313-win_arm64.whl", hash = "sha256:1e831894be7c2954240e49791fa4b50c05a0dc881de2552cfe3ffd8631c7f461"}, + {file = "yarl-1.24.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f9312b3c02d9b3d23840f67952913c9c8721d7f1b7db305289faefa878f364c2"}, + {file = "yarl-1.24.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a4f4d6cd615823bfc7fb7e9b5987c3f41666371d870d51058f77e2680fbe9630"}, + {file = "yarl-1.24.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0c3063e5c0a8e8e62fae6c2596fa01da1561e4cd1da6fec5789f5cf99a8aefd8"}, + {file = "yarl-1.24.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fecd17873a096036c1c87ab3486f1aef7f269ada7f23f7f856f93b1cc7744f14"}, + {file = "yarl-1.24.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a46d1ab4ba4d32e6dc80daf8a28ce0bd83d08df52fbc32f3e288663427734535"}, + {file = "yarl-1.24.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73e68edf6dfd5f73f9ca127d84e2a6f9213c65bdffb736bda19524c0564fcd14"}, + {file = "yarl-1.24.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a296ca617f2d25fbceafb962b88750d627e5984e75732c712154d058ae8d79a3"}, + {file = "yarl-1.24.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51b2cf5ec89a8b8470177641ed62a3ba22d74e1e898e06ad53aa77972487208"}, + {file = "yarl-1.24.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:310fc687f7b2044ec54e372c8cbe923bb88f5c37bded0d3079e5791c2fc3cf50"}, + {file = "yarl-1.24.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:297a2fe352ecf858b30a98f87948746ec16f001d279f84aebdbd3bd965e2f1bd"}, + {file = "yarl-1.24.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2a263e76b97bc42bdcd7c5f4953dec1f7cd62a1112fa7f869e57255229390d67"}, + {file = "yarl-1.24.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:822519b64cf0b474f1a0aaef1dc621438ea46bb77c94df97a5b4d213a7d8a8b1"}, + {file = "yarl-1.24.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b6067060d9dc594899ba83e6db6c48c68d1e494a6dab158156ed86977ca7bcb1"}, + {file = "yarl-1.24.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0063adad533e57171b79db3943b229d40dfafeeee579767f96541f106bac5f1b"}, + {file = "yarl-1.24.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ee8e3fb34513e8dc082b586ef4910c98335d43a6fab688cd44d4851bacfce3e8"}, + {file = "yarl-1.24.2-cp314-cp314-win_amd64.whl", hash = "sha256:afb00d7fd8e0f285ca29a44cc50df2d622ff2f7a6d933fa641577b5f9d5f3db0"}, + {file = "yarl-1.24.2-cp314-cp314-win_arm64.whl", hash = "sha256:68cf6eacd6028ef1142bc4b48376b81566385ca6f9e7dde3b0fa91be08ffcb57"}, + {file = "yarl-1.24.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:221ce1dd921ac4f603957f17d7c18c5cc0797fbb52f156941f92e04605d1d67b"}, + {file = "yarl-1.24.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5f3224db28173a00d7afacdee07045cc4673dfab2b15492c7ae10deddbece761"}, + {file = "yarl-1.24.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c557165320d6244ebe3a02431b2a201a20080e02f41f0cfa0ccc47a183765da8"}, + {file = "yarl-1.24.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:904065e6e85b1fa54d0d87438bd58c14c0bad97aad654ad1077fd9d87e8478ed"}, + {file = "yarl-1.24.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cec2a38d70edc10e0e856ceda886af5327a017ccbde8e1de1bd44d300357543"}, + {file = "yarl-1.24.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e7484b9361ed222ee1ca5b4337aa4cbdcc4618ce5aff57d9ef1582fd95893fc0"}, + {file = "yarl-1.24.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:84f9670b89f34db07f81e53aee83e0b938a3412329d51c8f922488be7fcc4024"}, + {file = "yarl-1.24.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:abb2759733d63a28b4956500a5dd57140f26486c92b2caedfb964ab7d9b79dbf"}, + {file = "yarl-1.24.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:081c2bf54efe03774d0311172bc04fedf9ca01e644d4cd8c805688e527209bdc"}, + {file = "yarl-1.24.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:86746bef442aa479107fe28132e1277237f9c24c2f00b0b0cf22b3ee0904f2bb"}, + {file = "yarl-1.24.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:2d07d21d0bc4b17558e8de0b02fbfdf1e347d3bb3699edd00bb92e7c57925420"}, + {file = "yarl-1.24.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4fb1ac3fc5fecd8ae7453ea237e4d22b49befa70266dfe1629924245c21a0c7f"}, + {file = "yarl-1.24.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4da31a5512ed1729ca8d8aacde3f7faeb8843cde3165d6bcf7f88f74f17bb8aa"}, + {file = "yarl-1.24.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:533ded4dceb5f1f3da7906244f4e82cf46cfd40d84c69a1faf5ac506aa65ecbe"}, + {file = "yarl-1.24.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7b3a85525f6e7eeabcfdd372862b21ee1915db1b498a04e8bf0e389b607ff0bd"}, + {file = "yarl-1.24.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a7624b1ca46ca5d7b864ef0d2f8efe3091454085ee1855b4e992314529972215"}, + {file = "yarl-1.24.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e434a45ce2e7a947f951fc5a8944c8cc080b7e59f9c50ae80fd39107cf88126d"}, + {file = "yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9"}, + {file = "yarl-1.24.2.tar.gz", hash = "sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" +propcache = ">=0.2.1" + +[extras] +aiohttp = ["aiohttp", "httpx-aiohttp"] + [metadata] -lock-version = "2.0" -python-versions = "^3.8" -content-hash = "6f6c191c1028d17a97fdfa84cedfd3cef94b5d63d98b8c1d333b3398eeea9055" +lock-version = "2.1" +python-versions = "^3.10" +content-hash = "ae72458cc9170491280c32063e114504d6d14e4e72794aefcc730f6d72478732" diff --git a/pyproject.toml b/pyproject.toml index ef5b406..66e382b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,10 @@ [project] name = "speechify-api" +dynamic = ["version"] [tool.poetry] name = "speechify-api" -version = "1.2.3" +version = "1.2.4" description = "Official Speechify API SDK" readme = "README.md" authors = [ @@ -22,11 +23,12 @@ classifiers = [ "Intended Audience :: Developers", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: MacOS", @@ -39,29 +41,37 @@ packages = [ { include = "speechify", from = "src"} ] -[project.urls] +[tool.poetry.urls] Documentation = 'https://docs.speechify.ai/api-reference' Homepage = 'https://docs.speechify.ai' Repository = 'https://github.com/speechifyinc/speechify-api-sdk-python' [tool.poetry.dependencies] -python = "^3.8" +python = "^3.10" +aiohttp = { version = ">=3.14.1,<4", optional = true, python = ">=3.10"} httpx = ">=0.21.2" +httpx-aiohttp = { version = "0.1.8", optional = true, python = ">=3.10"} pydantic = ">= 1.9.2" -pydantic-core = "^2.18.2" +pydantic-core = ">=2.18.2,<3.0.0" typing_extensions = ">= 4.0.0" -[tool.poetry.dev-dependencies] -mypy = "1.0.1" -pytest = "^7.4.0" -pytest-asyncio = "^0.23.5" +[tool.poetry.group.dev.dependencies] +mypy = "==1.13.0" +pytest = "^9.0.3" +pytest-asyncio = "^1.0.0" +pytest-xdist = "^3.6.1" python-dateutil = "^2.9.0" types-python-dateutil = "^2.9.0.20240316" -ruff = "^0.5.6" +urllib3 = ">=2.6.3,<3.0.0" +ruff = "==0.11.5" [tool.pytest.ini_options] testpaths = [ "tests" ] asyncio_mode = "auto" +norecursedirs = [ "src" ] +markers = [ + "aiohttp: tests that require httpx_aiohttp to be installed", +] [tool.mypy] plugins = ["pydantic.mypy"] @@ -69,7 +79,30 @@ plugins = ["pydantic.mypy"] [tool.ruff] line-length = 120 +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "F", # pyflakes + "I", # isort +] +ignore = [ + "E402", # Module level import not at top of file + "E501", # Line too long + "E711", # Comparison to `None` should be `cond is not None` + "E712", # Avoid equality comparisons to `True`; use `if ...:` checks + "E721", # Use `is` and `is not` for type comparisons, or `isinstance()` for insinstance checks + "E722", # Do not use bare `except` + "E731", # Do not assign a `lambda` expression, use a `def` + "F821", # Undefined name + "F841" # Local variable ... is assigned to but never used +] + +[tool.ruff.lint.isort] +section-order = ["future", "standard-library", "third-party", "first-party"] [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" + +[tool.poetry.extras] +aiohttp=["aiohttp", "httpx-aiohttp"] diff --git a/reference.md b/reference.md index 14e9f47..c65161b 100644 --- a/reference.md +++ b/reference.md @@ -1,6 +1,6 @@ # Reference -## Tts Audio -
client.tts.audio.speech(...) +## audio +
client.audio.speech(...) -> GetSpeechResponse
@@ -12,7 +12,9 @@
-Gets the speech data for the given input +Synthesize speech audio from text or SSML. Returns the complete audio +file plus billing and speech-mark metadata in a single JSON response. +For low-latency playback or long-form text, use POST /v1/audio/stream.
@@ -28,13 +30,18 @@ Gets the speech data for the given input ```python from speechify import Speechify +from speechify.environment import SpeechifyEnvironment client = Speechify( - token="YOUR_TOKEN", + api_key="", + environment=SpeechifyEnvironment.DEFAULT, ) -client.tts.audio.speech( - input="input", - voice_id="voice_id", + +client.audio.speech( + audio_format="mp3", + input="Hello! This is the Speechify text-to-speech API.", + model="simba-english", + voice_id="george", ) ``` @@ -90,10 +97,7 @@ Please refer to the list of the supported languages and recommendations regardin
-**model:** `typing.Optional[GetSpeechRequestModel]` - -Model used for audio synthesis. `simba-base` and `simba-turbo` are deprecated. -`simba-3.0` is the new streaming-native model with lower TTFB and richer expressivity. Currently English only; multilingual coming soon. Non-English voices return 400 until multilingual support ships. +**model:** `typing.Optional[GetSpeechRequestModel]` — Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.0` is the streaming-native model with lower TTFB and richer expressivity. Currently English only; multilingual coming soon. Non-English voices return 400 until multilingual support ships.
@@ -121,7 +125,7 @@ Model used for audio synthesis. `simba-base` and `simba-turbo` are deprecated.
-
client.tts.audio.stream(...) +
client.audio.stream(...) -> typing.Iterator[bytes]
@@ -133,7 +137,11 @@ Model used for audio synthesis. `simba-base` and `simba-turbo` are deprecated.
-Gets the stream speech for the given input +Synthesize speech and stream the audio back as it is generated, for +low-latency playback. The Accept header selects the audio container; +the response is raw audio bytes (HTTP chunked). For Base64-encoded +audio with speech-mark metadata in a single JSON response, use +POST /v1/audio/speech.
@@ -149,11 +157,14 @@ Gets the stream speech for the given input ```python from speechify import Speechify +from speechify.environment import SpeechifyEnvironment client = Speechify( - token="YOUR_TOKEN", + api_key="", + environment=SpeechifyEnvironment.DEFAULT, ) -client.tts.audio.stream( + +client.audio.stream( accept="audio/mpeg", input="input", voice_id="voice_id", @@ -174,6 +185,11 @@ client.tts.audio.stream(
**accept:** `StreamAudioRequestAccept` + +Selects the audio container/codec for the streamed response. The +response Content-Type echoes this value, except `audio/pcm` returns +`audio/L16` with rate and channels parameters (raw 16-bit linear +PCM, 24 kHz mono, little-endian).
@@ -212,10 +228,7 @@ Please refer to the list of the supported languages and recommendations regardin
-**model:** `typing.Optional[GetStreamRequestModel]` - -Model used for audio synthesis. `simba-base` and `simba-turbo` are deprecated. -`simba-3.0` is the new streaming-native model with lower TTFB and richer expressivity. Currently English only; multilingual coming soon. Non-English voices return 400 until multilingual support ships. +**model:** `typing.Optional[GetStreamRequestModel]` — Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.0` is the streaming-native model with lower TTFB and richer expressivity. Currently English only; multilingual coming soon. Non-English voices return 400 until multilingual support ships.
@@ -231,88 +244,6 @@ Model used for audio synthesis. `simba-base` and `simba-turbo` are deprecated.
-**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. You can pass in configuration such as `chunk_size`, and more to customize the request and response. - -
-
- - - - - - -
- -## Tts Auth -
client.tts.auth.create_access_token(...) -
-
- -#### 📝 Description - -
-
- -
-
- -WARNING: This endpoint is deprecated. Create a new API token for the logged in user. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.auth.create_access_token( - grant_type="client_credentials", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**grant_type:** `CreateAccessTokenRequestGrantType` — in: body - -
-
- -
-
- -**scope:** `typing.Optional[CreateAccessTokenRequestScope]` - -The scope, or a space-delimited list of scopes the token is requested for -in: body - -
-
- -
-
- **request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
@@ -325,8 +256,8 @@ in: body
-## Tts Voices -
client.tts.voices.list() +## voices +
client.voices.list() -> typing.List[GetVoice]
@@ -354,11 +285,14 @@ Gets the list of voices available for the user ```python from speechify import Speechify +from speechify.environment import SpeechifyEnvironment client = Speechify( - token="YOUR_TOKEN", + api_key="", + environment=SpeechifyEnvironment.DEFAULT, ) -client.tts.voices.list() + +client.voices.list() ```
@@ -386,7 +320,7 @@ client.tts.voices.list()
-
client.tts.voices.create(...) +
client.voices.create(...) -> CreatedVoice
@@ -414,11 +348,16 @@ Create a personal (cloned) voice for the user ```python from speechify import Speechify +from speechify.environment import SpeechifyEnvironment client = Speechify( - token="YOUR_TOKEN", + api_key="", + environment=SpeechifyEnvironment.DEFAULT, ) -client.tts.voices.create( + +client.voices.create( + sample="example_sample", + avatar="example_avatar", name="name", gender="male", consent="consent", @@ -459,9 +398,7 @@ notSpecified GenderNotSpecified
-**sample:** `from __future__ import annotations - -core.File` — See core.File for more documentation +**sample:** `core.File` — Audio sample file
@@ -489,9 +426,7 @@ For example, `{"fullName": "John Doe", "email": "john@example.com"}`
-**avatar:** `from __future__ import annotations - -typing.Optional[core.File]` — See core.File for more documentation +**avatar:** `typing.Optional[core.File]` — Avatar image file
@@ -511,7 +446,7 @@ typing.Optional[core.File]` — See core.File for more documentation
-
client.tts.voices.delete(...) +
client.voices.delete(...)
@@ -539,11 +474,14 @@ Delete a personal (cloned) voice ```python from speechify import Speechify +from speechify.environment import SpeechifyEnvironment client = Speechify( - token="YOUR_TOKEN", + api_key="", + environment=SpeechifyEnvironment.DEFAULT, ) -client.tts.voices.delete( + +client.voices.delete( id="id", ) @@ -581,7 +519,7 @@ client.tts.voices.delete(
-
client.tts.voices.download_sample(...) +
client.voices.download_sample(...) -> typing.Iterator[bytes]
@@ -609,11 +547,14 @@ Download a personal (cloned) voice sample ```python from speechify import Speechify +from speechify.environment import SpeechifyEnvironment client = Speechify( - token="YOUR_TOKEN", + api_key="", + environment=SpeechifyEnvironment.DEFAULT, ) -client.tts.voices.download_sample( + +client.voices.download_sample( id="id", ) @@ -639,6761 +580,6 @@ client.tts.voices.download_sample(
-**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. You can pass in configuration such as `chunk_size`, and more to customize the request and response. - -
-
-
-
- - - - -
- -## Tts Agents -
client.tts.agents.list() -
-
- -#### 📝 Description - -
-
- -
-
- -List voice agents owned by the caller. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.list() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.create(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Create a voice agent. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.create( - name="name", - voice_id="voice_id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**name:** `str` - -
-
- -
-
- -**voice_id:** `str` — Voice slug from the VMS catalog (see GET /v1/voices). Required — the server rejects writes with an unknown or empty slug. - -
-
- -
-
- -**slug:** `typing.Optional[str]` — Optional. Server derives slug from name with a random suffix when omitted; if you supply your own, a collision returns 400 'slug already taken'. - -
-
- -
-
- -**prompt:** `typing.Optional[str]` - -
-
- -
-
- -**first_message:** `typing.Optional[str]` — Spoken verbatim at session start — no LLM round trip. - -
-
- -
-
- -**language:** `typing.Optional[str]` - -
-
- -
-
- -**llm_model:** `typing.Optional[str]` — Optional chat model slug. Leave empty to use the Speechify default. - -
-
- -
-
- -**temperature:** `typing.Optional[float]` - -
-
- -
-
- -**config:** `typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]]` - -
-
- -
-
- -**is_public:** `typing.Optional[bool]` - -
-
- -
-
- -**allowed_origins:** `typing.Optional[typing.Sequence[str]]` - -
-
- -
-
- -**hostname_allowlist:** `typing.Optional[typing.Sequence[str]]` — Optional per-agent hostname allowlist (see Agent schema). - -
-
- -
-
- -**memory_enabled:** `typing.Optional[bool]` - -
-
- -
-
- -**memory_retention_days:** `typing.Optional[int]` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.get(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Retrieve a voice agent by ID. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.get( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.delete(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Delete a voice agent. Conversations and attached tools remain. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.delete( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.update(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Update a voice agent. Only fields present on the request body are changed. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.update( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**name:** `typing.Optional[str]` - -
-
- -
-
- -**prompt:** `typing.Optional[str]` - -
-
- -
-
- -**first_message:** `typing.Optional[str]` - -
-
- -
-
- -**language:** `typing.Optional[str]` - -
-
- -
-
- -**llm_model:** `typing.Optional[str]` - -
-
- -
-
- -**voice_id:** `typing.Optional[str]` - -
-
- -
-
- -**temperature:** `typing.Optional[float]` - -
-
- -
-
- -**config:** `typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]]` - -
-
- -
-
- -**is_public:** `typing.Optional[bool]` - -
-
- -
-
- -**allowed_origins:** `typing.Optional[typing.Sequence[str]]` - -
-
- -
-
- -**hostname_allowlist:** `typing.Optional[typing.Sequence[str]]` - -When supplied, replaces the stored list. Pass an empty -array to clear enforcement (public agent is open again). -Omit the field to leave the existing value unchanged. - -
-
- -
-
- -**memory_enabled:** `typing.Optional[bool]` - -
-
- -
-
- -**memory_retention_days:** `typing.Optional[int]` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.list_tools(...) -
-
- -#### 📝 Description - -
-
- -
-
- -List tools currently attached to the agent. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.list_tools( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.attach_tool(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Attach an existing tool to the agent so the LLM can call it. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.attach_tool( - id="id", - tool_id="toolId", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**tool_id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.detach_tool(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Detach a tool from the agent. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.detach_tool( - id="id", - tool_id="toolId", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**tool_id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.get_evaluation_config(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Retrieve the agent's post-call evaluation criteria + data-collection config. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.get_evaluation_config( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.update_evaluation_config(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Replace the agent's evaluation criteria + data-collection fields. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify -from speechify.tts import DataCollectionField, EvaluationCriterion - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.update_evaluation_config( - id="id", - criteria=[ - EvaluationCriterion( - id="id", - name="name", - description="description", - ) - ], - data_collection=[ - DataCollectionField( - key="key", - description="description", - type="string", - ) - ], -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**criteria:** `typing.Sequence[EvaluationCriterion]` - -
-
- -
-
- -**data_collection:** `typing.Sequence[DataCollectionField]` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.get_dynamic_variables(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Retrieve the agent's customer-scope dynamic variables and the read-only -catalogue of reserved `system__*` keys. The system variables list is -provided so editor UIs can render the reference list without maintaining -a client-side copy of the catalogue. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.get_dynamic_variables( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.update_dynamic_variables(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Replace the agent's customer-scope dynamic variable definitions. -The supplied list overwrites the stored list wholesale (same -semantics as `updateEvaluationConfig`). Pass an empty array to -clear all variables. Up to 20 variables per agent. Keys must -match `[a-zA-Z0-9_]+` and must not start with the reserved -`system__` prefix. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify -from speechify.tts import DynamicVariable - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.update_dynamic_variables( - id="id", - variables=[ - DynamicVariable( - key="product_name", - type="string", - default="Speechify", - description="Product the agent is supporting.", - ), - DynamicVariable( - key="support_tier", - type="number", - default=1, - ), - DynamicVariable( - key="account_metadata", - type="json", - description="Arbitrary account context injected into tool bodies.", - ), - ], -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**variables:** `typing.Sequence[DynamicVariable]` — The new variable list. Replaces the existing list entirely. - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.create_conversation(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Start a new voice conversation with the agent. Returns a realtime -voice session + short-lived client token so the caller can -connect the audio pipeline directly. The agent is dispatched -server-side; no additional client action required. - -Pass `dynamic_variables` to supply per-session values that override -the agent's stored variable defaults for this one conversation. -Keys in the `system__` namespace are rejected at this boundary. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.create_conversation( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**transport:** `typing.Optional[str]` — Transport hint. Omit to use the agent's default. - -
-
- -
-
- -**dynamic_variables:** `typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]]` - -Per-session variable overrides that merge on top of the agent's -stored variable defaults for this one conversation. Keys in the -reserved `system__` namespace are rejected. Values must match the -declared type of the corresponding variable definition on the agent. - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.create_session(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Mint a realtime voice session for the given agent. Widget-friendly -counterpart to `createConversation` — same response shape, dual -authentication: - -* **Authenticated (Bearer)**: works for any agent the caller - owns. Typical server-to-server flow where the embedding - site's backend mints a token and hands it to the browser so - the API key never reaches the client. -* **Unauthenticated**: works only when `agent.is_public = true` - AND the request's `Origin` header matches `agent.allowed_origins` - (or that list is empty). When `agent.hostname_allowlist` is - non-empty, the `Origin` hostname must additionally be a - member of that list. Used directly by the - `` web component. - -Responds with the same `CreateConversationResponse` as -`createConversation`. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.create_session( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**user_identity:** `typing.Optional[str]` — Opaque identifier for the end-user (e.g. your app's user ID). Stamped onto the conversation. Optional - defaults to an anonymous per-session ID. - -
-
- -
-
- -**dynamic_variables:** `typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]]` - -Per-session variable overrides that merge on top of the agent's -stored variable defaults for this one session. Keys in the -reserved `system__` namespace are rejected at this boundary. -Values must match the declared type of the corresponding variable -definition on the agent (a `string` type expects a JSON string, -`number` expects a JSON number, etc.). - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.list_agent_knowledge_bases(...) -
-
- -#### 📝 Description - -
-
- -
-
- -List knowledge bases attached to an agent. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.list_agent_knowledge_bases( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.attach_knowledge_base(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Attach a knowledge base to an agent. The `search_knowledge` tool -is auto-registered on the next conversation and can only query the -attached knowledge bases. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.attach_knowledge_base( - id="id", - kb_id="kbId", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**kb_id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.detach_knowledge_base(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Detach a knowledge base from an agent. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.detach_knowledge_base( - id="id", - kb_id="kbId", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**kb_id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.list_memories(...) -
-
- -#### 📝 Description - -
-
- -
-
- -List per-caller memories extracted for an agent. Memories are -written post-call by the built-in extractor when `memory_enabled` -is true on the agent; the list is sorted newest-first. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.list_memories( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**limit:** `typing.Optional[int]` — Maximum rows to return. Defaults to 100, capped at 200. - -
-
- -
-
- -**offset:** `typing.Optional[int]` — Number of rows to skip. Combine with `limit` to page through older memories. - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.delete_memories_by_caller(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Delete every memory ever extracted for a specific caller on -this agent. Privacy / GDPR surface. Returns the count of rows -soft-deleted; rows become permanently unreachable immediately -and are hard-deleted by the retention job after the tenant's -configured retention window. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.delete_memories_by_caller( - id="id", - agent_id="agent_id", - caller_identity="caller_identity", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**agent_id:** `str` - -
-
- -
-
- -**caller_identity:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.list_tests(...) -
-
- -#### 📝 Description - -
-
- -
-
- -List all tests configured for the agent. Each entry includes the -most recent run so the console can render pass/fail badges without -an extra round-trip. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.list_tests( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` — Agent ID. - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.create_test(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Create a new test for the agent. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify -from speechify.tts import ScenarioConfig - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.create_test( - id="id", - name="Greet the caller by name", - description="Agent should greet the caller using their name when provided.", - type="scenario", - config=ScenarioConfig( - context="The caller says: Hi, I'm Alice.", - success_criteria="The agent greets Alice by name.", - success_examples=["Hi Alice! How can I help you today?"], - failure_examples=["Hello! How can I help you?"], - ), -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` — Agent ID. - -
-
- -
-
- -**name:** `str` — Short human-readable label for the test. - -
-
- -
-
- -**type:** `TestType` - -
-
- -
-
- -**config:** `CreateAgentTestRequestConfig` — Type-specific configuration. Must match the shape for the given `type`. - -
-
- -
-
- -**description:** `typing.Optional[str]` — Optional longer description of what this test verifies. - -
-
- -
-
- -**tool_mock_config:** `typing.Optional[ToolMockConfig]` — Optional tool-mocking config applied during every run of this test. - -
-
- -
-
- -**variables:** `typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]]` - -Per-test variable values substituted into string fields of the -config at run-start. Keys use the same rules as agent-level -`DynamicVariable` keys. - -
-
- -
-
- -**folder_id:** `typing.Optional[str]` — Folder to place the test in. Omit for root. - -
-
- -
-
- -**attached_agent_ids:** `typing.Optional[typing.Sequence[str]]` - -Optional list of additional agents this test should also run -against. The owner agent (path param) is always attached -implicitly. - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.run_all_tests(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Enqueue runs for every test on the agent concurrently. Up to 50 -tests are dispatched in one call. Each returned run starts in -`queued` status; poll `GET /v1/test-runs/{id}` for the terminal -result. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.run_all_tests( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` — Agent ID. - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.get_test(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Retrieve a test by ID. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.get_test( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` — Test ID. - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.delete_test(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Delete a test and all its run history. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.delete_test( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` — Test ID. - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.update_test(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Update a test. Only fields present on the request body are changed. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.update_test( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` — Test ID. - -
-
- -
-
- -**name:** `typing.Optional[str]` - -
-
- -
-
- -**description:** `typing.Optional[str]` - -
-
- -
-
- -**config:** `typing.Optional[UpdateAgentTestRequestConfig]` — Replaces the test config when present. - -
-
- -
-
- -**tool_mock_config:** `typing.Optional[ToolMockConfig]` — Replaces the tool-mock config when present. - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.list_test_runs(...) -
-
- -#### 📝 Description - -
-
- -
-
- -List the run history for a test, newest first. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.list_test_runs( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` — Test ID. - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.run_test(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Enqueue a single run of the test. The returned run starts in -`queued` status. Poll `GET /v1/test-runs/{id}` until the status -reaches a terminal state (`passed`, `failed`, or `error`). -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.run_test( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` — Test ID. - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.get_test_run(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Retrieve a single test run by ID. Poll this endpoint until -`status` reaches a terminal state (`passed`, `failed`, or `error`). -The `result` field is populated on terminal states. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.get_test_run( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` — Test run ID. - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.list_all_tests(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Workspace-wide list of tests across every agent the caller owns. -Supports filters (agent, type, last-run status, folder), full-text -search on name/description, and cursor pagination. Each row carries -its newest run and attached agent IDs so the list renders without -N+1 round-trips. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.list_all_tests() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**agent_id:** `typing.Optional[str]` — Comma-separated agent IDs to filter on. - -
-
- -
-
- -**type:** `typing.Optional[str]` — Comma-separated test types (scenario|tool|simulation). - -
-
- -
-
- -**status:** `typing.Optional[str]` — Comma-separated last-run statuses. - -
-
- -
-
- -**folder_id:** `typing.Optional[str]` — Folder ID to filter on, or "root" for unfiled tests. - -
-
- -
-
- -**updated_after:** `typing.Optional[str]` — Only return tests updated after this RFC3339 timestamp. - -
-
- -
-
- -**q:** `typing.Optional[str]` — Substring match on name or description. - -
-
- -
-
- -**limit:** `typing.Optional[int]` — Max tests per page (default 50, max 200). - -
-
- -
-
- -**cursor:** `typing.Optional[str]` — Opaque pagination cursor from a previous response. - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.get_test_stats(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Aggregate pass-rate metrics over the last N days. Returns dense -daily buckets (one entry per day, zero-filled) plus totals and a -per-type breakdown. Powers the header chart on the global tests -page. Default window is 30 days, max 90. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.get_test_stats() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**window_days:** `typing.Optional[int]` — Trailing window in days (default 30, max 90). - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.run_tests_batch(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Queue runs for every (test, agent) pair in the body. Entries -without an `agent_id` fan out to every agent the test is -attached to. Total expanded runs are capped at 100 per call. -Each entry in the response is a queued run; poll -`GET /v1/test-runs/{id}` for each. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify -from speechify.tts import BatchRunEntry - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.run_tests_batch( - entries=[ - BatchRunEntry( - test_id="test_id", - ) - ], -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**entries:** `typing.Sequence[BatchRunEntry]` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.list_test_attachments(...) -
-
- -#### 📝 Description - -
-
- -
-
- -List every agent a test is attached to. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.list_test_attachments( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.attach_test(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Attach a test to an additional agent. After this call, the test -will also run as part of that agent's regression suite (and -against its prompt + tool config when invoked with -`agent_id = {agentId}`). Idempotent. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.attach_test( - id="id", - agent_id="agentId", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**agent_id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.detach_test(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Detach a test from an agent. The owner agent (the agent the test -was authored against) cannot be detached; delete the test -instead. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.detach_test( - id="id", - agent_id="agentId", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**agent_id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.move_test(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Move a test into a folder. Pass `folder_id: null` for root. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.move_test( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**folder_id:** `typing.Optional[str]` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.list_test_folders() -
-
- -#### 📝 Description - -
-
- -
-
- -List every test folder the caller owns. Flat list; build the tree client-side. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.list_test_folders() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.create_test_folder(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Create a test folder. Max depth is 3. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.create_test_folder( - name="name", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**name:** `str` - -
-
- -
-
- -**parent_folder_id:** `typing.Optional[str]` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.delete_test_folder(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Soft-delete a folder. Child tests drop back to root. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.delete_test_folder( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.agents.update_test_folder(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Rename or reparent a test folder. Cycles are rejected. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.agents.update_test_folder( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**name:** `typing.Optional[str]` - -
-
- -
-
- -**parent_folder_id:** `typing.Optional[str]` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## Tts Tools -
client.tts.tools.list() -
-
- -#### 📝 Description - -
-
- -
-
- -List tools owned by the caller. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.tools.list() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.tools.create(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Create a tool. For webhook tools, the response includes the HMAC -`webhook_secret` exactly once — store it immediately; subsequent -reads return a masked placeholder. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify -from speechify.tts import SystemToolConfig - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.tools.create( - name="name", - description="description", - kind="system", - config=SystemToolConfig( - builtin="end_call", - ), -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**name:** `str` - -
-
- -
-
- -**description:** `str` - -
-
- -
-
- -**kind:** `ToolKind` - -
-
- -
-
- -**config:** `CreateToolRequestConfig` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.tools.get(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Retrieve a tool by ID. Webhook secrets are always masked here. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.tools.get( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.tools.delete(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Delete a tool. Agents that had it attached get a soft-detach. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.tools.delete( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.tools.update(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Update a tool. Tool kind is immutable — create a new tool to change it. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.tools.update( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**name:** `typing.Optional[str]` - -
-
- -
-
- -**description:** `typing.Optional[str]` - -
-
- -
-
- -**config:** `typing.Optional[UpdateToolRequestConfig]` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## Tts Conversations -
client.tts.conversations.list() -
-
- -#### 📝 Description - -
-
- -
-
- -List conversations owned by the caller, ordered by most recent. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.conversations.list() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.conversations.stats() -
-
- -#### 📝 Description - -
-
- -
-
- -Aggregated counts and averages over the caller's conversations, scoped by the same filters as the list endpoint. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.conversations.stats() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.conversations.get(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Retrieve a conversation by ID. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.conversations.get( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.conversations.list_messages(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Retrieve the full transcript for a conversation, in order. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.conversations.list_messages( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.conversations.list_evaluations(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Retrieve post-call evaluation results for a conversation. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.conversations.list_evaluations( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.conversations.list_memories(...) -
-
- -#### 📝 Description - -
-
- -
-
- -List memories extracted from a specific conversation. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.conversations.list_memories( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## Tts KnowledgeBases -
client.tts.knowledge_bases.list() -
-
- -#### 📝 Description - -
-
- -
-
- -List knowledge bases owned by the caller. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.knowledge_bases.list() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.knowledge_bases.create(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Create a new knowledge base. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.knowledge_bases.create( - name="name", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**name:** `str` — Human-readable label. - -
-
- -
-
- -**description:** `typing.Optional[str]` — Optional description. - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.knowledge_bases.get(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Retrieve a knowledge base by ID. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.knowledge_bases.get( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.knowledge_bases.delete(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Soft-delete a knowledge base. Documents and chunks are cascaded. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.knowledge_bases.delete( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.knowledge_bases.update(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Update a knowledge base. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.knowledge_bases.update( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**name:** `typing.Optional[str]` - -
-
- -
-
- -**description:** `typing.Optional[str]` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.knowledge_bases.list_documents(...) -
-
- -#### 📝 Description - -
-
- -
-
- -List documents ingested into a knowledge base. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.knowledge_bases.list_documents( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.knowledge_bases.upload_document(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Upload a document (PDF, plain text, markdown, or HTML) to a -knowledge base. The document is extracted, chunked, embedded, and -indexed synchronously; expect a few seconds per MB of input. -Maximum 10 MB per upload. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.knowledge_bases.upload_document( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**file:** `from __future__ import annotations - -core.File` — See core.File for more documentation - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.knowledge_bases.get_document(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Retrieve a document by ID. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.knowledge_bases.get_document( - doc_id="docId", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**doc_id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.knowledge_bases.delete_document(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Delete a document and all its chunks. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.knowledge_bases.delete_document( - doc_id="docId", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**doc_id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.knowledge_bases.list_chunks(...) -
-
- -#### 📝 Description - -
-
- -
-
- -List the chunks for a document. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.knowledge_bases.list_chunks( - doc_id="docId", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**doc_id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.knowledge_bases.search(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Semantic search across a caller-owned list of knowledge bases. -Returns ranked chunks with source filename and a cosine-similarity -score. Limited to 50 results per request. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.knowledge_bases.search( - query="query", - kb_ids=["kb_ids"], -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**query:** `str` — Natural-language search query. - -
-
- -
-
- -**kb_ids:** `typing.Sequence[str]` — Knowledge bases to search across. Results scoped to caller-owned entries; unknown IDs are silently ignored. - -
-
- -
-
- -**top_k:** `typing.Optional[int]` — Max hits to return (default 5, capped at 50). - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## Tts Memories -
client.tts.memories.delete(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Soft-delete one memory row. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.memories.delete( - memory_id="memoryId", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**memory_id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## Tts PhoneNumbers -
client.tts.phone_numbers.list() -
-
- -#### 📝 Description - -
-
- -
-
- -List all phone numbers in the caller's workspace. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.phone_numbers.list() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.phone_numbers.import_(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Import a phone number into the workspace. The `source` field -determines the provisioning path: - -- `livekit` - LiveKit purchases the number on your behalf. US - inbound only. Quickest path for local testing. -- `twilio` - Provide your Twilio Account SID, Auth Token, and - the E.164 number you already own. We provision an Elastic SIP - Trunk on your Twilio account automatically. -- `byoc` - Provide an existing SIP trunk ID. The number is - registered against that trunk. - -Returns 402 when the workspace has reached the 100-number cap. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.phone_numbers.import_( - request={"key": "value"}, -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request:** `typing.Optional[typing.Any]` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.phone_numbers.get(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Retrieve a phone number by ID. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.phone_numbers.get( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.phone_numbers.delete(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Delete a phone number from the workspace. For Twilio and LiveKit -numbers this also deprovisions the backing SIP trunk and dispatch -rule on LiveKit Cloud. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.phone_numbers.delete( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.phone_numbers.update(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Update a phone number. Only `label` and `agent_id` are mutable; -`source` and `e164` are immutable after import. Pass `null` for -`agent_id` to unbind the number from its current agent. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.phone_numbers.update( - id="id", - request={"key": "value"}, -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**request:** `typing.Optional[typing.Any]` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## Tts SipTrunks -
client.tts.sip_trunks.list() -
-
- -#### 📝 Description - -
-
- -
-
- -List all SIP trunks in the caller's workspace. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.sip_trunks.list() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.sip_trunks.create(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Create a SIP trunk. For `kind=byoc` supply `sip_address` plus -optional digest credentials and IP allowlist. For `kind=twilio` -use `ImportPhoneNumber` with a `twilio` spec instead - trunk -creation is handled automatically. Returns 402 when the workspace -has reached the 20-trunk cap. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.sip_trunks.create( - request={"key": "value"}, -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request:** `typing.Optional[typing.Any]` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.sip_trunks.get(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Retrieve a SIP trunk by ID. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.sip_trunks.get( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.sip_trunks.delete(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Delete a SIP trunk. This also removes the backing LiveKit inbound -trunk, outbound trunk, and dispatch rule if they were provisioned -by us. Phone numbers attached to this trunk are left in place but -become non-functional until rebound to a new trunk. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.sip_trunks.delete( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## Tts OutboundCalls -
client.tts.outbound_calls.create(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Place an outbound call from an agent to a phone number. LiveKit -originates the SIP INVITE through the outbound trunk bound to the -agent's workspace; the agent worker is dispatched into the room -automatically. - -The response is returned as soon as LiveKit accepts the INVITE. -Poll `GET /v1/conversations/{conversation_id}` for status -transitions: `pending` → `active` (answered) → `completed`. - -Requires a Twilio or BYOC trunk. LiveKit-native numbers are -inbound-only. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.outbound_calls.create( - request={"key": "value"}, -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request:** `typing.Optional[typing.Any]` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## Tts Workspaces -
client.tts.workspaces.list() -
-
- -#### 📝 Description - -
-
- -
-
- -List every workspace the authenticated user belongs to. Powers the workspace switcher. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.workspaces.list() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.workspaces.create(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Create a new workspace with the authenticated user as owner. -The caller must switch their active workspace client-side via -the `X-Tenant-ID` header to act on the new tenant. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.workspaces.create() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**name:** `typing.Optional[str]` — Display name for the new workspace. Trimmed; must be 120 characters or fewer. - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.workspaces.get_current() -
-
- -#### 📝 Description - -
-
- -
-
- -Retrieve the workspace currently selected by the caller (via `X-Tenant-ID` or auto-resolved). -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.workspaces.get_current() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.workspaces.update_current(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Rename the current workspace. Owner or admin only. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.workspaces.update_current( - name="name", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**name:** `str` — New display name. Required; must be 120 characters or fewer. - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.workspaces.list_members() -
-
- -#### 📝 Description - -
-
- -
-
- -List every member of the current workspace. Any member may call this. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.workspaces.list_members() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.workspaces.leave() -
-
- -#### 📝 Description - -
-
- -
-
- -Remove the authenticated caller from the current workspace. -Refused with 409 when the caller is the last owner — promote -another member to owner first. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.workspaces.leave() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.workspaces.remove_member(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Remove a member from the current workspace. Owner or admin -only. The caller cannot remove themselves — use -`POST /v1/tenants/current/members/leave` instead. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.workspaces.remove_member( - user_uid="user_uid", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**user_uid:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.workspaces.update_member_role(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Change a member's role. Owner only — admins may add or remove -members but may not change roles. Refused with 409 when -demoting the last remaining owner. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.workspaces.update_member_role( - user_uid="user_uid", - role="owner", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**user_uid:** `str` - -
-
- -
-
- -**role:** `MemberRole` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.workspaces.list_invites() -
-
- -#### 📝 Description - -
-
- -
-
- -List outstanding invites for the current workspace. Invite tokens are redacted. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.workspaces.list_invites() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.workspaces.create_invite(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Create an invite to the current workspace. Owner or admin only. -The response contains the invite token ONCE — subsequent list -calls redact it. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.workspaces.create_invite( - email="email", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**email:** `str` — Email of the person to invite. Validated as an RFC 5322 address. - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.workspaces.revoke_invite(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Revoke an outstanding invite. Owner or admin only. Idempotent. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.workspaces.revoke_invite( - id="id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**id:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.workspaces.accept_invite(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Accept a workspace invite. The authenticated caller is joined -to the invite's workspace as a member. Expired, revoked, or -already-accepted tokens return 404 to avoid token enumeration. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.workspaces.accept_invite( - token="token", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**token:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.workspaces.preview_invite(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Preview a workspace invite without authenticating. Returns the -workspace name, inviter details, and expiry so the `/join/{token}` -page can render before the recipient signs in. Anyone with the -token can already accept, so this endpoint deliberately surfaces -the same information a caller would see after accepting. Invalid -tokens (unknown, expired, revoked, already-accepted, or pointing -at a soft-deleted workspace) collapse to a single 404 to prevent -enumeration. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.workspaces.preview_invite( - token="token", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**token:** `str` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.tts.workspaces.transfer_workspace_owner(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Transfer ownership of the current workspace atomically. Promotes -the target member to owner and demotes the caller to admin in a -single transaction. Owner-only; admins cannot hand off a role -they were never granted. Prefer this over two PATCH calls to -`/v1/tenants/current/members/{user_uid}`: a sole-owner caller -cannot demote themselves first without tripping the last-owner -guard, which this endpoint sidesteps by promoting before -demoting. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from speechify import Speechify - -client = Speechify( - token="YOUR_TOKEN", -) -client.tts.workspaces.transfer_workspace_owner( - user_uid="user_uid", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**user_uid:** `str` — Firebase UID of the member who will become the new owner. - -
-
- -
-
- **request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
diff --git a/requirements.txt b/requirements.txt index f502f1b..443b1af 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ httpx>=0.21.2 pydantic>= 1.9.2 -pydantic-core==^2.18.2 +pydantic-core>=2.18.2,<3.0.0 typing_extensions>= 4.0.0 diff --git a/src/speechify/__init__.py b/src/speechify/__init__.py index c35fd6b..48b4289 100644 --- a/src/speechify/__init__.py +++ b/src/speechify/__init__.py @@ -1,8 +1,170 @@ # This file was auto-generated by Fern from our API Definition. -from . import tts -from .client import AsyncSpeechify, Speechify -from .environment import SpeechifyEnvironment -from .version import __version__ +# isort: skip_file -__all__ = ["AsyncSpeechify", "Speechify", "SpeechifyEnvironment", "__version__", "tts"] +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import ( + CreateVoiceLanguage, + CreateVoiceModel, + CreateVoiceModelName, + CreatedVoice, + CreatedVoiceGender, + CreatedVoiceType, + Error, + ErrorCode, + ErrorDetail, + GetSpeechOptionsRequest, + GetSpeechResponse, + GetSpeechResponseAudioFormat, + GetStreamOptionsRequest, + GetVoice, + GetVoiceGender, + GetVoiceLanguage, + GetVoiceType, + GetVoicesModel, + GetVoicesModelName, + NestedChunk, + SpeechMarks, + ) + from .errors import ( + BadGatewayError, + BadRequestError, + ForbiddenError, + InternalServerError, + NotFoundError, + PaymentRequiredError, + ServiceUnavailableError, + TooManyRequestsError, + UnauthorizedError, + UnprocessableEntityError, + ) + from . import audio, voices + from ._default_clients import DefaultAioHttpClient, DefaultAsyncHttpxClient + from .audio import ( + GetSpeechRequestAudioFormat, + GetSpeechRequestModel, + GetStreamRequestModel, + StreamAudioRequestAccept, + ) + from .client import AsyncSpeechify, Speechify + from .environment import SpeechifyEnvironment + from .version import __version__ + from .voices import CreateVoicesRequestGender +_dynamic_imports: typing.Dict[str, str] = { + "AsyncSpeechify": ".client", + "BadGatewayError": ".errors", + "BadRequestError": ".errors", + "CreateVoiceLanguage": ".types", + "CreateVoiceModel": ".types", + "CreateVoiceModelName": ".types", + "CreateVoicesRequestGender": ".voices", + "CreatedVoice": ".types", + "CreatedVoiceGender": ".types", + "CreatedVoiceType": ".types", + "DefaultAioHttpClient": "._default_clients", + "DefaultAsyncHttpxClient": "._default_clients", + "Error": ".types", + "ErrorCode": ".types", + "ErrorDetail": ".types", + "ForbiddenError": ".errors", + "GetSpeechOptionsRequest": ".types", + "GetSpeechRequestAudioFormat": ".audio", + "GetSpeechRequestModel": ".audio", + "GetSpeechResponse": ".types", + "GetSpeechResponseAudioFormat": ".types", + "GetStreamOptionsRequest": ".types", + "GetStreamRequestModel": ".audio", + "GetVoice": ".types", + "GetVoiceGender": ".types", + "GetVoiceLanguage": ".types", + "GetVoiceType": ".types", + "GetVoicesModel": ".types", + "GetVoicesModelName": ".types", + "InternalServerError": ".errors", + "NestedChunk": ".types", + "NotFoundError": ".errors", + "PaymentRequiredError": ".errors", + "ServiceUnavailableError": ".errors", + "SpeechMarks": ".types", + "Speechify": ".client", + "SpeechifyEnvironment": ".environment", + "StreamAudioRequestAccept": ".audio", + "TooManyRequestsError": ".errors", + "UnauthorizedError": ".errors", + "UnprocessableEntityError": ".errors", + "__version__": ".version", + "audio": ".audio", + "voices": ".voices", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "AsyncSpeechify", + "BadGatewayError", + "BadRequestError", + "CreateVoiceLanguage", + "CreateVoiceModel", + "CreateVoiceModelName", + "CreateVoicesRequestGender", + "CreatedVoice", + "CreatedVoiceGender", + "CreatedVoiceType", + "DefaultAioHttpClient", + "DefaultAsyncHttpxClient", + "Error", + "ErrorCode", + "ErrorDetail", + "ForbiddenError", + "GetSpeechOptionsRequest", + "GetSpeechRequestAudioFormat", + "GetSpeechRequestModel", + "GetSpeechResponse", + "GetSpeechResponseAudioFormat", + "GetStreamOptionsRequest", + "GetStreamRequestModel", + "GetVoice", + "GetVoiceGender", + "GetVoiceLanguage", + "GetVoiceType", + "GetVoicesModel", + "GetVoicesModelName", + "InternalServerError", + "NestedChunk", + "NotFoundError", + "PaymentRequiredError", + "ServiceUnavailableError", + "SpeechMarks", + "Speechify", + "SpeechifyEnvironment", + "StreamAudioRequestAccept", + "TooManyRequestsError", + "UnauthorizedError", + "UnprocessableEntityError", + "__version__", + "audio", + "voices", +] diff --git a/src/speechify/_default_clients.py b/src/speechify/_default_clients.py new file mode 100644 index 0000000..108087d --- /dev/null +++ b/src/speechify/_default_clients.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx + +SDK_DEFAULT_TIMEOUT = 60 + +try: + import httpx_aiohttp # type: ignore[import-not-found] +except ImportError: + + class DefaultAioHttpClient(httpx.AsyncClient): # type: ignore + def __init__(self, **kwargs: typing.Any) -> None: + raise RuntimeError( + "To use the aiohttp client, install the aiohttp extra: pip install speechify-api[aiohttp]" + ) + +else: + + class DefaultAioHttpClient(httpx_aiohttp.HttpxAiohttpClient): # type: ignore + def __init__(self, **kwargs: typing.Any) -> None: + kwargs.setdefault("timeout", SDK_DEFAULT_TIMEOUT) + kwargs.setdefault("follow_redirects", True) + super().__init__(**kwargs) + + +class DefaultAsyncHttpxClient(httpx.AsyncClient): + def __init__(self, **kwargs: typing.Any) -> None: + kwargs.setdefault("timeout", SDK_DEFAULT_TIMEOUT) + kwargs.setdefault("follow_redirects", True) + super().__init__(**kwargs) diff --git a/src/speechify/audio/__init__.py b/src/speechify/audio/__init__.py new file mode 100644 index 0000000..3167496 --- /dev/null +++ b/src/speechify/audio/__init__.py @@ -0,0 +1,44 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import ( + GetSpeechRequestAudioFormat, + GetSpeechRequestModel, + GetStreamRequestModel, + StreamAudioRequestAccept, + ) +_dynamic_imports: typing.Dict[str, str] = { + "GetSpeechRequestAudioFormat": ".types", + "GetSpeechRequestModel": ".types", + "GetStreamRequestModel": ".types", + "StreamAudioRequestAccept": ".types", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["GetSpeechRequestAudioFormat", "GetSpeechRequestModel", "GetStreamRequestModel", "StreamAudioRequestAccept"] diff --git a/src/speechify/audio/client.py b/src/speechify/audio/client.py new file mode 100644 index 0000000..033c1c6 --- /dev/null +++ b/src/speechify/audio/client.py @@ -0,0 +1,358 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from ..types.get_speech_options_request import GetSpeechOptionsRequest +from ..types.get_speech_response import GetSpeechResponse +from ..types.get_stream_options_request import GetStreamOptionsRequest +from .raw_client import AsyncRawAudioClient, RawAudioClient +from .types.get_speech_request_audio_format import GetSpeechRequestAudioFormat +from .types.get_speech_request_model import GetSpeechRequestModel +from .types.get_stream_request_model import GetStreamRequestModel +from .types.stream_audio_request_accept import StreamAudioRequestAccept + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class AudioClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawAudioClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawAudioClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawAudioClient + """ + return self._raw_client + + def speech( + self, + *, + input: str, + voice_id: str, + audio_format: typing.Optional[GetSpeechRequestAudioFormat] = OMIT, + language: typing.Optional[str] = OMIT, + model: typing.Optional[GetSpeechRequestModel] = OMIT, + options: typing.Optional[GetSpeechOptionsRequest] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetSpeechResponse: + """ + Synthesize speech audio from text or SSML. Returns the complete audio + file plus billing and speech-mark metadata in a single JSON response. + For low-latency playback or long-form text, use POST /v1/audio/stream. + + Parameters + ---------- + input : str + Plain text or SSML to be synthesized to speech. + Refer to https://docs.speechify.ai/docs/api-limits for the input size limits. + Emotion, Pitch and Speed Rate are configured in the ssml input, please refer to the ssml documentation for more information: https://docs.speechify.ai/docs/ssml#prosody + + voice_id : str + Id of the voice to be used for synthesizing speech. Refer to /v1/voices endpoint for available voices + + audio_format : typing.Optional[GetSpeechRequestAudioFormat] + The format for the output audio. Note, that the current default is "wav", but there's no guarantee it will not change in the future. We recommend always passing the specific param you expect. + + language : typing.Optional[str] + Language of the input. Follow the format of an ISO 639-1 language code and an ISO 3166-1 region code, separated by a hyphen, e.g. en-US. + Please refer to the list of the supported languages and recommendations regarding this parameter: https://docs.speechify.ai/docs/language-support. + + model : typing.Optional[GetSpeechRequestModel] + Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.0` is the streaming-native model with lower TTFB and richer expressivity. Currently English only; multilingual coming soon. Non-English voices return 400 until multilingual support ships. + + options : typing.Optional[GetSpeechOptionsRequest] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetSpeechResponse + Synthesized speech audio for the requested input. + + Examples + -------- + from speechify import Speechify + + client = Speechify( + api_key="YOUR_API_KEY", + ) + client.audio.speech( + audio_format="mp3", + input="Hello! This is the Speechify text-to-speech API.", + model="simba-english", + voice_id="george", + ) + """ + _response = self._raw_client.speech( + input=input, + voice_id=voice_id, + audio_format=audio_format, + language=language, + model=model, + options=options, + request_options=request_options, + ) + return _response.data + + def stream( + self, + *, + accept: StreamAudioRequestAccept, + input: str, + voice_id: str, + language: typing.Optional[str] = OMIT, + model: typing.Optional[GetStreamRequestModel] = OMIT, + options: typing.Optional[GetStreamOptionsRequest] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> typing.Iterator[bytes]: + """ + Synthesize speech and stream the audio back as it is generated, for + low-latency playback. The Accept header selects the audio container; + the response is raw audio bytes (HTTP chunked). For Base64-encoded + audio with speech-mark metadata in a single JSON response, use + POST /v1/audio/speech. + + Parameters + ---------- + accept : StreamAudioRequestAccept + Selects the audio container/codec for the streamed response. The + response Content-Type echoes this value, except `audio/pcm` returns + `audio/L16` with rate and channels parameters (raw 16-bit linear + PCM, 24 kHz mono, little-endian). + + input : str + Plain text or SSML to be synthesized to speech. + Refer to https://docs.speechify.ai/docs/api-limits for the input size limits. + Emotion, Pitch and Speed Rate are configured in the ssml input, please refer to the ssml documentation for more information: https://docs.speechify.ai/docs/ssml#prosody + + voice_id : str + Id of the voice to be used for synthesizing speech. Refer to /v1/voices endpoint for available voices + + language : typing.Optional[str] + Language of the input. Follow the format of an ISO 639-1 language code and an ISO 3166-1 region code, separated by a hyphen, e.g. en-US. + Please refer to the list of the supported languages and recommendations regarding this parameter: https://docs.speechify.ai/docs/language-support. + + model : typing.Optional[GetStreamRequestModel] + Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.0` is the streaming-native model with lower TTFB and richer expressivity. Currently English only; multilingual coming soon. Non-English voices return 400 until multilingual support ships. + + options : typing.Optional[GetStreamOptionsRequest] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. You can pass in configuration such as `chunk_size`, and more to customize the request and response. + + Returns + ------- + typing.Iterator[bytes] + Chunked audio stream for the requested input. + + Examples + -------- + from speechify import Speechify + + client = Speechify( + api_key="YOUR_API_KEY", + ) + client.audio.stream( + accept="audio/mpeg", + input="input", + voice_id="voice_id", + ) + """ + with self._raw_client.stream( + accept=accept, + input=input, + voice_id=voice_id, + language=language, + model=model, + options=options, + request_options=request_options, + ) as r: + yield from r.data + + +class AsyncAudioClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawAudioClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawAudioClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawAudioClient + """ + return self._raw_client + + async def speech( + self, + *, + input: str, + voice_id: str, + audio_format: typing.Optional[GetSpeechRequestAudioFormat] = OMIT, + language: typing.Optional[str] = OMIT, + model: typing.Optional[GetSpeechRequestModel] = OMIT, + options: typing.Optional[GetSpeechOptionsRequest] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> GetSpeechResponse: + """ + Synthesize speech audio from text or SSML. Returns the complete audio + file plus billing and speech-mark metadata in a single JSON response. + For low-latency playback or long-form text, use POST /v1/audio/stream. + + Parameters + ---------- + input : str + Plain text or SSML to be synthesized to speech. + Refer to https://docs.speechify.ai/docs/api-limits for the input size limits. + Emotion, Pitch and Speed Rate are configured in the ssml input, please refer to the ssml documentation for more information: https://docs.speechify.ai/docs/ssml#prosody + + voice_id : str + Id of the voice to be used for synthesizing speech. Refer to /v1/voices endpoint for available voices + + audio_format : typing.Optional[GetSpeechRequestAudioFormat] + The format for the output audio. Note, that the current default is "wav", but there's no guarantee it will not change in the future. We recommend always passing the specific param you expect. + + language : typing.Optional[str] + Language of the input. Follow the format of an ISO 639-1 language code and an ISO 3166-1 region code, separated by a hyphen, e.g. en-US. + Please refer to the list of the supported languages and recommendations regarding this parameter: https://docs.speechify.ai/docs/language-support. + + model : typing.Optional[GetSpeechRequestModel] + Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.0` is the streaming-native model with lower TTFB and richer expressivity. Currently English only; multilingual coming soon. Non-English voices return 400 until multilingual support ships. + + options : typing.Optional[GetSpeechOptionsRequest] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetSpeechResponse + Synthesized speech audio for the requested input. + + Examples + -------- + import asyncio + + from speechify import AsyncSpeechify + + client = AsyncSpeechify( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.audio.speech( + audio_format="mp3", + input="Hello! This is the Speechify text-to-speech API.", + model="simba-english", + voice_id="george", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.speech( + input=input, + voice_id=voice_id, + audio_format=audio_format, + language=language, + model=model, + options=options, + request_options=request_options, + ) + return _response.data + + async def stream( + self, + *, + accept: StreamAudioRequestAccept, + input: str, + voice_id: str, + language: typing.Optional[str] = OMIT, + model: typing.Optional[GetStreamRequestModel] = OMIT, + options: typing.Optional[GetStreamOptionsRequest] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> typing.AsyncIterator[bytes]: + """ + Synthesize speech and stream the audio back as it is generated, for + low-latency playback. The Accept header selects the audio container; + the response is raw audio bytes (HTTP chunked). For Base64-encoded + audio with speech-mark metadata in a single JSON response, use + POST /v1/audio/speech. + + Parameters + ---------- + accept : StreamAudioRequestAccept + Selects the audio container/codec for the streamed response. The + response Content-Type echoes this value, except `audio/pcm` returns + `audio/L16` with rate and channels parameters (raw 16-bit linear + PCM, 24 kHz mono, little-endian). + + input : str + Plain text or SSML to be synthesized to speech. + Refer to https://docs.speechify.ai/docs/api-limits for the input size limits. + Emotion, Pitch and Speed Rate are configured in the ssml input, please refer to the ssml documentation for more information: https://docs.speechify.ai/docs/ssml#prosody + + voice_id : str + Id of the voice to be used for synthesizing speech. Refer to /v1/voices endpoint for available voices + + language : typing.Optional[str] + Language of the input. Follow the format of an ISO 639-1 language code and an ISO 3166-1 region code, separated by a hyphen, e.g. en-US. + Please refer to the list of the supported languages and recommendations regarding this parameter: https://docs.speechify.ai/docs/language-support. + + model : typing.Optional[GetStreamRequestModel] + Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.0` is the streaming-native model with lower TTFB and richer expressivity. Currently English only; multilingual coming soon. Non-English voices return 400 until multilingual support ships. + + options : typing.Optional[GetStreamOptionsRequest] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. You can pass in configuration such as `chunk_size`, and more to customize the request and response. + + Returns + ------- + typing.AsyncIterator[bytes] + Chunked audio stream for the requested input. + + Examples + -------- + import asyncio + + from speechify import AsyncSpeechify + + client = AsyncSpeechify( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.audio.stream( + accept="audio/mpeg", + input="input", + voice_id="voice_id", + ) + + + asyncio.run(main()) + """ + async with self._raw_client.stream( + accept=accept, + input=input, + voice_id=voice_id, + language=language, + model=model, + options=options, + request_options=request_options, + ) as r: + async for _chunk in r.data: + yield _chunk diff --git a/src/speechify/audio/raw_client.py b/src/speechify/audio/raw_client.py new file mode 100644 index 0000000..e0f5483 --- /dev/null +++ b/src/speechify/audio/raw_client.py @@ -0,0 +1,801 @@ +# This file was auto-generated by Fern from our API Definition. + +import contextlib +import typing +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.parse_error import ParsingError +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from ..core.serialization import convert_and_respect_annotation_metadata +from ..errors.bad_gateway_error import BadGatewayError +from ..errors.bad_request_error import BadRequestError +from ..errors.forbidden_error import ForbiddenError +from ..errors.internal_server_error import InternalServerError +from ..errors.not_found_error import NotFoundError +from ..errors.payment_required_error import PaymentRequiredError +from ..errors.service_unavailable_error import ServiceUnavailableError +from ..errors.too_many_requests_error import TooManyRequestsError +from ..errors.unauthorized_error import UnauthorizedError +from ..types.error import Error +from ..types.get_speech_options_request import GetSpeechOptionsRequest +from ..types.get_speech_response import GetSpeechResponse +from ..types.get_stream_options_request import GetStreamOptionsRequest +from .types.get_speech_request_audio_format import GetSpeechRequestAudioFormat +from .types.get_speech_request_model import GetSpeechRequestModel +from .types.get_stream_request_model import GetStreamRequestModel +from .types.stream_audio_request_accept import StreamAudioRequestAccept +from pydantic import ValidationError + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawAudioClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def speech( + self, + *, + input: str, + voice_id: str, + audio_format: typing.Optional[GetSpeechRequestAudioFormat] = OMIT, + language: typing.Optional[str] = OMIT, + model: typing.Optional[GetSpeechRequestModel] = OMIT, + options: typing.Optional[GetSpeechOptionsRequest] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[GetSpeechResponse]: + """ + Synthesize speech audio from text or SSML. Returns the complete audio + file plus billing and speech-mark metadata in a single JSON response. + For low-latency playback or long-form text, use POST /v1/audio/stream. + + Parameters + ---------- + input : str + Plain text or SSML to be synthesized to speech. + Refer to https://docs.speechify.ai/docs/api-limits for the input size limits. + Emotion, Pitch and Speed Rate are configured in the ssml input, please refer to the ssml documentation for more information: https://docs.speechify.ai/docs/ssml#prosody + + voice_id : str + Id of the voice to be used for synthesizing speech. Refer to /v1/voices endpoint for available voices + + audio_format : typing.Optional[GetSpeechRequestAudioFormat] + The format for the output audio. Note, that the current default is "wav", but there's no guarantee it will not change in the future. We recommend always passing the specific param you expect. + + language : typing.Optional[str] + Language of the input. Follow the format of an ISO 639-1 language code and an ISO 3166-1 region code, separated by a hyphen, e.g. en-US. + Please refer to the list of the supported languages and recommendations regarding this parameter: https://docs.speechify.ai/docs/language-support. + + model : typing.Optional[GetSpeechRequestModel] + Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.0` is the streaming-native model with lower TTFB and richer expressivity. Currently English only; multilingual coming soon. Non-English voices return 400 until multilingual support ships. + + options : typing.Optional[GetSpeechOptionsRequest] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetSpeechResponse] + Synthesized speech audio for the requested input. + """ + _response = self._client_wrapper.httpx_client.request( + "v1/audio/speech", + method="POST", + json={ + "audio_format": audio_format, + "input": input, + "language": language, + "model": model, + "options": convert_and_respect_annotation_metadata( + object_=options, annotation=GetSpeechOptionsRequest, direction="write" + ), + "voice_id": voice_id, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetSpeechResponse, + parse_obj_as( + type_=GetSpeechResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 502: + raise BadGatewayError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 503: + raise ServiceUnavailableError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + @contextlib.contextmanager + def stream( + self, + *, + accept: StreamAudioRequestAccept, + input: str, + voice_id: str, + language: typing.Optional[str] = OMIT, + model: typing.Optional[GetStreamRequestModel] = OMIT, + options: typing.Optional[GetStreamOptionsRequest] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> typing.Iterator[HttpResponse[typing.Iterator[bytes]]]: + """ + Synthesize speech and stream the audio back as it is generated, for + low-latency playback. The Accept header selects the audio container; + the response is raw audio bytes (HTTP chunked). For Base64-encoded + audio with speech-mark metadata in a single JSON response, use + POST /v1/audio/speech. + + Parameters + ---------- + accept : StreamAudioRequestAccept + Selects the audio container/codec for the streamed response. The + response Content-Type echoes this value, except `audio/pcm` returns + `audio/L16` with rate and channels parameters (raw 16-bit linear + PCM, 24 kHz mono, little-endian). + + input : str + Plain text or SSML to be synthesized to speech. + Refer to https://docs.speechify.ai/docs/api-limits for the input size limits. + Emotion, Pitch and Speed Rate are configured in the ssml input, please refer to the ssml documentation for more information: https://docs.speechify.ai/docs/ssml#prosody + + voice_id : str + Id of the voice to be used for synthesizing speech. Refer to /v1/voices endpoint for available voices + + language : typing.Optional[str] + Language of the input. Follow the format of an ISO 639-1 language code and an ISO 3166-1 region code, separated by a hyphen, e.g. en-US. + Please refer to the list of the supported languages and recommendations regarding this parameter: https://docs.speechify.ai/docs/language-support. + + model : typing.Optional[GetStreamRequestModel] + Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.0` is the streaming-native model with lower TTFB and richer expressivity. Currently English only; multilingual coming soon. Non-English voices return 400 until multilingual support ships. + + options : typing.Optional[GetStreamOptionsRequest] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. You can pass in configuration such as `chunk_size`, and more to customize the request and response. + + Returns + ------- + typing.Iterator[HttpResponse[typing.Iterator[bytes]]] + Chunked audio stream for the requested input. + """ + with self._client_wrapper.httpx_client.stream( + "v1/audio/stream", + method="POST", + json={ + "input": input, + "language": language, + "model": model, + "options": convert_and_respect_annotation_metadata( + object_=options, annotation=GetStreamOptionsRequest, direction="write" + ), + "voice_id": voice_id, + }, + headers={ + "content-type": "application/json", + "Accept": str(accept) if accept is not None else None, + }, + request_options=request_options, + omit=OMIT, + ) as _response: + + def _stream() -> HttpResponse[typing.Iterator[bytes]]: + try: + if 200 <= _response.status_code < 300: + _chunk_size = request_options.get("chunk_size", None) if request_options is not None else None + return HttpResponse( + response=_response, data=(_chunk for _chunk in _response.iter_bytes(chunk_size=_chunk_size)) + ) + _response.read() + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 502: + raise BadGatewayError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 503: + raise ServiceUnavailableError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.text + ) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.json(), + cause=e, + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + yield _stream() + + +class AsyncRawAudioClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def speech( + self, + *, + input: str, + voice_id: str, + audio_format: typing.Optional[GetSpeechRequestAudioFormat] = OMIT, + language: typing.Optional[str] = OMIT, + model: typing.Optional[GetSpeechRequestModel] = OMIT, + options: typing.Optional[GetSpeechOptionsRequest] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[GetSpeechResponse]: + """ + Synthesize speech audio from text or SSML. Returns the complete audio + file plus billing and speech-mark metadata in a single JSON response. + For low-latency playback or long-form text, use POST /v1/audio/stream. + + Parameters + ---------- + input : str + Plain text or SSML to be synthesized to speech. + Refer to https://docs.speechify.ai/docs/api-limits for the input size limits. + Emotion, Pitch and Speed Rate are configured in the ssml input, please refer to the ssml documentation for more information: https://docs.speechify.ai/docs/ssml#prosody + + voice_id : str + Id of the voice to be used for synthesizing speech. Refer to /v1/voices endpoint for available voices + + audio_format : typing.Optional[GetSpeechRequestAudioFormat] + The format for the output audio. Note, that the current default is "wav", but there's no guarantee it will not change in the future. We recommend always passing the specific param you expect. + + language : typing.Optional[str] + Language of the input. Follow the format of an ISO 639-1 language code and an ISO 3166-1 region code, separated by a hyphen, e.g. en-US. + Please refer to the list of the supported languages and recommendations regarding this parameter: https://docs.speechify.ai/docs/language-support. + + model : typing.Optional[GetSpeechRequestModel] + Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.0` is the streaming-native model with lower TTFB and richer expressivity. Currently English only; multilingual coming soon. Non-English voices return 400 until multilingual support ships. + + options : typing.Optional[GetSpeechOptionsRequest] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetSpeechResponse] + Synthesized speech audio for the requested input. + """ + _response = await self._client_wrapper.httpx_client.request( + "v1/audio/speech", + method="POST", + json={ + "audio_format": audio_format, + "input": input, + "language": language, + "model": model, + "options": convert_and_respect_annotation_metadata( + object_=options, annotation=GetSpeechOptionsRequest, direction="write" + ), + "voice_id": voice_id, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetSpeechResponse, + parse_obj_as( + type_=GetSpeechResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 502: + raise BadGatewayError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 503: + raise ServiceUnavailableError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + @contextlib.asynccontextmanager + async def stream( + self, + *, + accept: StreamAudioRequestAccept, + input: str, + voice_id: str, + language: typing.Optional[str] = OMIT, + model: typing.Optional[GetStreamRequestModel] = OMIT, + options: typing.Optional[GetStreamOptionsRequest] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> typing.AsyncIterator[AsyncHttpResponse[typing.AsyncIterator[bytes]]]: + """ + Synthesize speech and stream the audio back as it is generated, for + low-latency playback. The Accept header selects the audio container; + the response is raw audio bytes (HTTP chunked). For Base64-encoded + audio with speech-mark metadata in a single JSON response, use + POST /v1/audio/speech. + + Parameters + ---------- + accept : StreamAudioRequestAccept + Selects the audio container/codec for the streamed response. The + response Content-Type echoes this value, except `audio/pcm` returns + `audio/L16` with rate and channels parameters (raw 16-bit linear + PCM, 24 kHz mono, little-endian). + + input : str + Plain text or SSML to be synthesized to speech. + Refer to https://docs.speechify.ai/docs/api-limits for the input size limits. + Emotion, Pitch and Speed Rate are configured in the ssml input, please refer to the ssml documentation for more information: https://docs.speechify.ai/docs/ssml#prosody + + voice_id : str + Id of the voice to be used for synthesizing speech. Refer to /v1/voices endpoint for available voices + + language : typing.Optional[str] + Language of the input. Follow the format of an ISO 639-1 language code and an ISO 3166-1 region code, separated by a hyphen, e.g. en-US. + Please refer to the list of the supported languages and recommendations regarding this parameter: https://docs.speechify.ai/docs/language-support. + + model : typing.Optional[GetStreamRequestModel] + Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.0` is the streaming-native model with lower TTFB and richer expressivity. Currently English only; multilingual coming soon. Non-English voices return 400 until multilingual support ships. + + options : typing.Optional[GetStreamOptionsRequest] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. You can pass in configuration such as `chunk_size`, and more to customize the request and response. + + Returns + ------- + typing.AsyncIterator[AsyncHttpResponse[typing.AsyncIterator[bytes]]] + Chunked audio stream for the requested input. + """ + async with self._client_wrapper.httpx_client.stream( + "v1/audio/stream", + method="POST", + json={ + "input": input, + "language": language, + "model": model, + "options": convert_and_respect_annotation_metadata( + object_=options, annotation=GetStreamOptionsRequest, direction="write" + ), + "voice_id": voice_id, + }, + headers={ + "content-type": "application/json", + "Accept": str(accept) if accept is not None else None, + }, + request_options=request_options, + omit=OMIT, + ) as _response: + + async def _stream() -> AsyncHttpResponse[typing.AsyncIterator[bytes]]: + try: + if 200 <= _response.status_code < 300: + _chunk_size = request_options.get("chunk_size", None) if request_options is not None else None + return AsyncHttpResponse( + response=_response, + data=(_chunk async for _chunk in _response.aiter_bytes(chunk_size=_chunk_size)), + ) + await _response.aread() + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 502: + raise BadGatewayError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 503: + raise ServiceUnavailableError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.text + ) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.json(), + cause=e, + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + yield await _stream() diff --git a/src/speechify/audio/types/__init__.py b/src/speechify/audio/types/__init__.py new file mode 100644 index 0000000..0d1a756 --- /dev/null +++ b/src/speechify/audio/types/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .get_speech_request_audio_format import GetSpeechRequestAudioFormat + from .get_speech_request_model import GetSpeechRequestModel + from .get_stream_request_model import GetStreamRequestModel + from .stream_audio_request_accept import StreamAudioRequestAccept +_dynamic_imports: typing.Dict[str, str] = { + "GetSpeechRequestAudioFormat": ".get_speech_request_audio_format", + "GetSpeechRequestModel": ".get_speech_request_model", + "GetStreamRequestModel": ".get_stream_request_model", + "StreamAudioRequestAccept": ".stream_audio_request_accept", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["GetSpeechRequestAudioFormat", "GetSpeechRequestModel", "GetStreamRequestModel", "StreamAudioRequestAccept"] diff --git a/src/speechify/tts/audio/types/get_speech_request_audio_format.py b/src/speechify/audio/types/get_speech_request_audio_format.py similarity index 100% rename from src/speechify/tts/audio/types/get_speech_request_audio_format.py rename to src/speechify/audio/types/get_speech_request_audio_format.py diff --git a/src/speechify/audio/types/get_speech_request_model.py b/src/speechify/audio/types/get_speech_request_model.py new file mode 100644 index 0000000..da407de --- /dev/null +++ b/src/speechify/audio/types/get_speech_request_model.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +GetSpeechRequestModel = typing.Union[typing.Literal["simba-english", "simba-multilingual", "simba-3.0"], typing.Any] diff --git a/src/speechify/audio/types/get_stream_request_model.py b/src/speechify/audio/types/get_stream_request_model.py new file mode 100644 index 0000000..76d1582 --- /dev/null +++ b/src/speechify/audio/types/get_stream_request_model.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +GetStreamRequestModel = typing.Union[typing.Literal["simba-english", "simba-multilingual", "simba-3.0"], typing.Any] diff --git a/src/speechify/tts/audio/types/stream_audio_request_accept.py b/src/speechify/audio/types/stream_audio_request_accept.py similarity index 100% rename from src/speechify/tts/audio/types/stream_audio_request_accept.py rename to src/speechify/audio/types/stream_audio_request_accept.py diff --git a/src/speechify/client.py b/src/speechify/client.py index 909afb0..4f9beee 100644 --- a/src/speechify/client.py +++ b/src/speechify/client.py @@ -1,13 +1,19 @@ # This file was auto-generated by Fern from our API Definition. -import typing -from .environment import SpeechifyEnvironment +from __future__ import annotations + import os +import typing + import httpx -from .core.client_wrapper import SyncClientWrapper -from .tts.client import TtsClient -from .core.client_wrapper import AsyncClientWrapper -from .tts.client import AsyncTtsClient +from .core.api_error import ApiError +from .core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from .core.logging import LogConfig, Logger +from .environment import SpeechifyEnvironment + +if typing.TYPE_CHECKING: + from .audio.client import AsyncAudioClient, AudioClient + from .voices.client import AsyncVoicesClient, VoicesClient class Speechify: @@ -28,22 +34,31 @@ class Speechify: - token : typing.Optional[typing.Union[str, typing.Callable[[], str]]] + api_key : typing.Optional[typing.Union[str, typing.Callable[[], str]]] + headers : typing.Optional[typing.Dict[str, str]] + Additional headers to send with every request. + timeout : typing.Optional[float] The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. + max_retries : typing.Optional[int] + The default maximum number of retries for failed requests. Defaults to 2. Per-request `max_retries` in `request_options` takes precedence over this value. + follow_redirects : typing.Optional[bool] Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. httpx_client : typing.Optional[httpx.Client] The httpx client to use for making requests, a preconfigured client is used by default, however this is useful should you want to pass in any custom httpx configuration. + logging : typing.Optional[typing.Union[LogConfig, Logger]] + Configure logging for the SDK. Accepts a LogConfig dict with 'level' (debug/info/warn/error), 'logger' (custom logger implementation), and 'silent' (boolean, defaults to True) fields. You can also pass a pre-configured Logger instance. + Examples -------- from speechify import Speechify client = Speechify( - token="YOUR_TOKEN", + api_key="YOUR_API_KEY", ) """ @@ -52,23 +67,71 @@ def __init__( *, base_url: typing.Optional[str] = None, environment: SpeechifyEnvironment = SpeechifyEnvironment.DEFAULT, - token: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = os.getenv("SPEECHIFY_API_KEY"), + api_key: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = os.getenv("SPEECHIFY_API_KEY"), + headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, + max_retries: typing.Optional[int] = None, follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.Client] = None, + logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, ): - _defaulted_timeout = timeout if timeout is not None else 60 if httpx_client is None else None + _defaulted_timeout = ( + timeout if timeout is not None else 60 if httpx_client is None else httpx_client.timeout.read + ) + _defaulted_max_retries = max_retries if max_retries is not None else 2 + if api_key is None: + raise ApiError( + body="The client must be instantiated be either passing in api_key or setting SPEECHIFY_API_KEY" + ) self._client_wrapper = SyncClientWrapper( base_url=_get_base_url(base_url=base_url, environment=environment), - token=token, + api_key=api_key, + headers=headers, httpx_client=httpx_client if httpx_client is not None else httpx.Client(timeout=_defaulted_timeout, follow_redirects=follow_redirects) if follow_redirects is not None else httpx.Client(timeout=_defaulted_timeout), timeout=_defaulted_timeout, + max_retries=_defaulted_max_retries, + logging=logging, ) - self.tts = TtsClient(client_wrapper=self._client_wrapper) + self._audio: typing.Optional[AudioClient] = None + self._voices: typing.Optional[VoicesClient] = None + + @property + def audio(self): + if self._audio is None: + from .audio.client import AudioClient # noqa: E402 + + self._audio = AudioClient(client_wrapper=self._client_wrapper) + return self._audio + + @property + def voices(self): + if self._voices is None: + from .voices.client import VoicesClient # noqa: E402 + + self._voices = VoicesClient(client_wrapper=self._client_wrapper) + return self._voices + + +def _make_default_async_client( + timeout: typing.Optional[float], + follow_redirects: typing.Optional[bool], +) -> httpx.AsyncClient: + try: + import httpx_aiohttp # type: ignore[import-not-found] + except ImportError: + pass + else: + if follow_redirects is not None: + return httpx_aiohttp.HttpxAiohttpClient(timeout=timeout, follow_redirects=follow_redirects) + return httpx_aiohttp.HttpxAiohttpClient(timeout=timeout) + + if follow_redirects is not None: + return httpx.AsyncClient(timeout=timeout, follow_redirects=follow_redirects) + return httpx.AsyncClient(timeout=timeout) class AsyncSpeechify: @@ -89,22 +152,34 @@ class AsyncSpeechify: - token : typing.Optional[typing.Union[str, typing.Callable[[], str]]] + api_key : typing.Optional[typing.Union[str, typing.Callable[[], str]]] + headers : typing.Optional[typing.Dict[str, str]] + Additional headers to send with every request. + + async_token : typing.Optional[typing.Callable[[], typing.Awaitable[str]]] + An async callable that returns a bearer token. Use this when token acquisition involves async I/O (e.g., refreshing tokens via an async HTTP client). When provided, this is used instead of the synchronous token for async requests. + timeout : typing.Optional[float] The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. + max_retries : typing.Optional[int] + The default maximum number of retries for failed requests. Defaults to 2. Per-request `max_retries` in `request_options` takes precedence over this value. + follow_redirects : typing.Optional[bool] Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. httpx_client : typing.Optional[httpx.AsyncClient] The httpx client to use for making requests, a preconfigured client is used by default, however this is useful should you want to pass in any custom httpx configuration. + logging : typing.Optional[typing.Union[LogConfig, Logger]] + Configure logging for the SDK. Accepts a LogConfig dict with 'level' (debug/info/warn/error), 'logger' (custom logger implementation), and 'silent' (boolean, defaults to True) fields. You can also pass a pre-configured Logger instance. + Examples -------- from speechify import AsyncSpeechify client = AsyncSpeechify( - token="YOUR_TOKEN", + api_key="YOUR_API_KEY", ) """ @@ -113,23 +188,53 @@ def __init__( *, base_url: typing.Optional[str] = None, environment: SpeechifyEnvironment = SpeechifyEnvironment.DEFAULT, - token: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = os.getenv("SPEECHIFY_API_KEY"), + api_key: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = os.getenv("SPEECHIFY_API_KEY"), + headers: typing.Optional[typing.Dict[str, str]] = None, + async_token: typing.Optional[typing.Callable[[], typing.Awaitable[str]]] = None, timeout: typing.Optional[float] = None, + max_retries: typing.Optional[int] = None, follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.AsyncClient] = None, + logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, ): - _defaulted_timeout = timeout if timeout is not None else 60 if httpx_client is None else None + _defaulted_timeout = ( + timeout if timeout is not None else 60 if httpx_client is None else httpx_client.timeout.read + ) + _defaulted_max_retries = max_retries if max_retries is not None else 2 + if api_key is None: + raise ApiError( + body="The client must be instantiated be either passing in api_key or setting SPEECHIFY_API_KEY" + ) self._client_wrapper = AsyncClientWrapper( base_url=_get_base_url(base_url=base_url, environment=environment), - token=token, + api_key=api_key, + headers=headers, + async_token=async_token, httpx_client=httpx_client if httpx_client is not None - else httpx.AsyncClient(timeout=_defaulted_timeout, follow_redirects=follow_redirects) - if follow_redirects is not None - else httpx.AsyncClient(timeout=_defaulted_timeout), + else _make_default_async_client(timeout=_defaulted_timeout, follow_redirects=follow_redirects), timeout=_defaulted_timeout, + max_retries=_defaulted_max_retries, + logging=logging, ) - self.tts = AsyncTtsClient(client_wrapper=self._client_wrapper) + self._audio: typing.Optional[AsyncAudioClient] = None + self._voices: typing.Optional[AsyncVoicesClient] = None + + @property + def audio(self): + if self._audio is None: + from .audio.client import AsyncAudioClient # noqa: E402 + + self._audio = AsyncAudioClient(client_wrapper=self._client_wrapper) + return self._audio + + @property + def voices(self): + if self._voices is None: + from .voices.client import AsyncVoicesClient # noqa: E402 + + self._voices = AsyncVoicesClient(client_wrapper=self._client_wrapper) + return self._voices def _get_base_url(*, base_url: typing.Optional[str] = None, environment: SpeechifyEnvironment) -> str: diff --git a/src/speechify/core/__init__.py b/src/speechify/core/__init__.py index f03aecb..5bc159a 100644 --- a/src/speechify/core/__init__.py +++ b/src/speechify/core/__init__.py @@ -1,43 +1,123 @@ # This file was auto-generated by Fern from our API Definition. -from .api_error import ApiError -from .client_wrapper import AsyncClientWrapper, BaseClientWrapper, SyncClientWrapper -from .datetime_utils import serialize_datetime -from .file import File, convert_file_dict_to_httpx_tuples, with_content_type -from .http_client import AsyncHttpClient, HttpClient -from .jsonable_encoder import jsonable_encoder -from .pydantic_utilities import ( - IS_PYDANTIC_V2, - UniversalBaseModel, - UniversalRootModel, - parse_obj_as, - universal_field_validator, - universal_root_validator, - update_forward_refs, -) -from .query_encoder import encode_query -from .remove_none_from_dict import remove_none_from_dict -from .request_options import RequestOptions -from .serialization import FieldMetadata, convert_and_respect_annotation_metadata +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .api_error import ApiError + from .client_wrapper import AsyncClientWrapper, BaseClientWrapper, SyncClientWrapper + from .datetime_utils import Rfc2822DateTime, parse_rfc2822_datetime, serialize_datetime + from .file import File, convert_file_dict_to_httpx_tuples, with_content_type + from .http_client import AsyncHttpClient, HttpClient + from .http_response import AsyncHttpResponse, HttpResponse + from .jsonable_encoder import encode_path_param, jsonable_encoder + from .logging import ConsoleLogger, ILogger, LogConfig, LogLevel, Logger, create_logger + from .parse_error import ParsingError + from .pydantic_utilities import ( + IS_PYDANTIC_V2, + UniversalBaseModel, + UniversalRootModel, + parse_obj_as, + universal_field_validator, + universal_root_validator, + update_forward_refs, + ) + from .query_encoder import encode_query + from .remove_none_from_dict import remove_none_from_dict + from .request_options import RequestOptions + from .serialization import FieldMetadata, convert_and_respect_annotation_metadata +_dynamic_imports: typing.Dict[str, str] = { + "ApiError": ".api_error", + "AsyncClientWrapper": ".client_wrapper", + "AsyncHttpClient": ".http_client", + "AsyncHttpResponse": ".http_response", + "BaseClientWrapper": ".client_wrapper", + "ConsoleLogger": ".logging", + "FieldMetadata": ".serialization", + "File": ".file", + "HttpClient": ".http_client", + "HttpResponse": ".http_response", + "ILogger": ".logging", + "IS_PYDANTIC_V2": ".pydantic_utilities", + "LogConfig": ".logging", + "LogLevel": ".logging", + "Logger": ".logging", + "ParsingError": ".parse_error", + "RequestOptions": ".request_options", + "Rfc2822DateTime": ".datetime_utils", + "SyncClientWrapper": ".client_wrapper", + "UniversalBaseModel": ".pydantic_utilities", + "UniversalRootModel": ".pydantic_utilities", + "convert_and_respect_annotation_metadata": ".serialization", + "convert_file_dict_to_httpx_tuples": ".file", + "create_logger": ".logging", + "encode_path_param": ".jsonable_encoder", + "encode_query": ".query_encoder", + "jsonable_encoder": ".jsonable_encoder", + "parse_obj_as": ".pydantic_utilities", + "parse_rfc2822_datetime": ".datetime_utils", + "remove_none_from_dict": ".remove_none_from_dict", + "serialize_datetime": ".datetime_utils", + "universal_field_validator": ".pydantic_utilities", + "universal_root_validator": ".pydantic_utilities", + "update_forward_refs": ".pydantic_utilities", + "with_content_type": ".file", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + __all__ = [ "ApiError", "AsyncClientWrapper", "AsyncHttpClient", + "AsyncHttpResponse", "BaseClientWrapper", + "ConsoleLogger", "FieldMetadata", "File", "HttpClient", + "HttpResponse", + "ILogger", "IS_PYDANTIC_V2", + "LogConfig", + "LogLevel", + "Logger", + "ParsingError", "RequestOptions", + "Rfc2822DateTime", "SyncClientWrapper", "UniversalBaseModel", "UniversalRootModel", "convert_and_respect_annotation_metadata", "convert_file_dict_to_httpx_tuples", + "create_logger", + "encode_path_param", "encode_query", "jsonable_encoder", "parse_obj_as", + "parse_rfc2822_datetime", "remove_none_from_dict", "serialize_datetime", "universal_field_validator", diff --git a/src/speechify/core/api_error.py b/src/speechify/core/api_error.py index 2e9fc54..6f850a6 100644 --- a/src/speechify/core/api_error.py +++ b/src/speechify/core/api_error.py @@ -1,15 +1,23 @@ # This file was auto-generated by Fern from our API Definition. -import typing +from typing import Any, Dict, Optional class ApiError(Exception): - status_code: typing.Optional[int] - body: typing.Any + headers: Optional[Dict[str, str]] + status_code: Optional[int] + body: Any - def __init__(self, *, status_code: typing.Optional[int] = None, body: typing.Any = None): + def __init__( + self, + *, + headers: Optional[Dict[str, str]] = None, + status_code: Optional[int] = None, + body: Any = None, + ) -> None: + self.headers = headers self.status_code = status_code self.body = body def __str__(self) -> str: - return f"status_code: {self.status_code}, body: {self.body}" + return f"headers: {self.headers}, status_code: {self.status_code}, body: {self.body}" diff --git a/src/speechify/core/client_wrapper.py b/src/speechify/core/client_wrapper.py index 1c0ca85..4e7910d 100644 --- a/src/speechify/core/client_wrapper.py +++ b/src/speechify/core/client_wrapper.py @@ -1,39 +1,53 @@ # This file was auto-generated by Fern from our API Definition. import typing + import httpx -from .http_client import HttpClient -from .http_client import AsyncHttpClient +from .http_client import AsyncHttpClient, HttpClient +from .logging import LogConfig, Logger class BaseClientWrapper: def __init__( self, *, - token: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, + api_key: typing.Union[str, typing.Callable[[], str]], + headers: typing.Optional[typing.Dict[str, str]] = None, base_url: str, timeout: typing.Optional[float] = None, + max_retries: int = 2, + logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, ): - self._token = token + self._api_key = api_key + self._headers = headers self._base_url = base_url self._timeout = timeout + self._max_retries = max_retries + self._logging = logging def get_headers(self) -> typing.Dict[str, str]: + import platform + headers: typing.Dict[str, str] = { + "User-Agent": "speechify-api/1.2.4", "X-Fern-Language": "Python", + "X-Fern-Runtime": f"python/{platform.python_version()}", + "X-Fern-Platform": f"{platform.system().lower()}/{platform.release()}", "X-Fern-SDK-Name": "speechify-api", - "X-Fern-SDK-Version": "1.2.3", # x-release-please-version + "X-Fern-SDK-Version": "1.2.4", + **(self.get_custom_headers() or {}), } - token = self._get_token() - if token is not None: - headers["Authorization"] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {self._get_api_key()}" return headers - def _get_token(self) -> typing.Optional[str]: - if isinstance(self._token, str) or self._token is None: - return self._token + def _get_api_key(self) -> str: + if isinstance(self._api_key, str): + return self._api_key else: - return self._token() + return self._api_key() + + def get_custom_headers(self) -> typing.Optional[typing.Dict[str, str]]: + return self._headers def get_base_url(self) -> str: return self._base_url @@ -41,22 +55,37 @@ def get_base_url(self) -> str: def get_timeout(self) -> typing.Optional[float]: return self._timeout + def get_max_retries(self) -> int: + return self._max_retries + class SyncClientWrapper(BaseClientWrapper): def __init__( self, *, - token: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, + api_key: typing.Union[str, typing.Callable[[], str]], + headers: typing.Optional[typing.Dict[str, str]] = None, base_url: str, timeout: typing.Optional[float] = None, + max_retries: int = 2, + logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, httpx_client: httpx.Client, ): - super().__init__(token=token, base_url=base_url, timeout=timeout) + super().__init__( + api_key=api_key, + headers=headers, + base_url=base_url, + timeout=timeout, + max_retries=max_retries, + logging=logging, + ) self.httpx_client = HttpClient( httpx_client=httpx_client, base_headers=self.get_headers, base_timeout=self.get_timeout, base_url=self.get_base_url, + base_max_retries=self.get_max_retries(), + logging_config=self._logging, ) @@ -64,15 +93,37 @@ class AsyncClientWrapper(BaseClientWrapper): def __init__( self, *, - token: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, + api_key: typing.Union[str, typing.Callable[[], str]], + headers: typing.Optional[typing.Dict[str, str]] = None, base_url: str, timeout: typing.Optional[float] = None, + max_retries: int = 2, + logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, + async_token: typing.Optional[typing.Callable[[], typing.Awaitable[str]]] = None, httpx_client: httpx.AsyncClient, ): - super().__init__(token=token, base_url=base_url, timeout=timeout) + super().__init__( + api_key=api_key, + headers=headers, + base_url=base_url, + timeout=timeout, + max_retries=max_retries, + logging=logging, + ) + self._async_token = async_token self.httpx_client = AsyncHttpClient( httpx_client=httpx_client, base_headers=self.get_headers, base_timeout=self.get_timeout, base_url=self.get_base_url, + base_max_retries=self.get_max_retries(), + async_base_headers=self.async_get_headers, + logging_config=self._logging, ) + + async def async_get_headers(self) -> typing.Dict[str, str]: + headers = self.get_headers() + if self._async_token is not None: + token = await self._async_token() + headers["Authorization"] = f"Bearer {token}" + return headers diff --git a/src/speechify/core/datetime_utils.py b/src/speechify/core/datetime_utils.py index 7c9864a..a12b2ad 100644 --- a/src/speechify/core/datetime_utils.py +++ b/src/speechify/core/datetime_utils.py @@ -1,6 +1,48 @@ # This file was auto-generated by Fern from our API Definition. import datetime as dt +from email.utils import parsedate_to_datetime +from typing import Any + +import pydantic + +IS_PYDANTIC_V2 = pydantic.VERSION.startswith("2.") + + +def parse_rfc2822_datetime(v: Any) -> dt.datetime: + """ + Parse an RFC 2822 datetime string (e.g., "Wed, 02 Oct 2002 13:00:00 GMT") + into a datetime object. If the value is already a datetime, return it as-is. + Falls back to ISO 8601 parsing if RFC 2822 parsing fails. + """ + if isinstance(v, dt.datetime): + return v + if isinstance(v, str): + try: + return parsedate_to_datetime(v) + except Exception: + pass + # Fallback to ISO 8601 parsing + return dt.datetime.fromisoformat(v.replace("Z", "+00:00")) + raise ValueError(f"Expected str or datetime, got {type(v)}") + + +class Rfc2822DateTime(dt.datetime): + """A datetime subclass that parses RFC 2822 date strings. + + On Pydantic V1, uses __get_validators__ for pre-validation. + On Pydantic V2, uses __get_pydantic_core_schema__ for BeforeValidator-style parsing. + """ + + @classmethod + def __get_validators__(cls): # type: ignore[no-untyped-def] + yield parse_rfc2822_datetime + + @classmethod + def __get_pydantic_core_schema__(cls, _source_type: Any, _handler: Any) -> Any: # type: ignore[override] + from pydantic_core import core_schema + + return core_schema.no_info_before_validator_function(parse_rfc2822_datetime, core_schema.datetime_schema()) def serialize_datetime(v: dt.datetime) -> str: diff --git a/src/speechify/core/force_multipart.py b/src/speechify/core/force_multipart.py new file mode 100644 index 0000000..5440913 --- /dev/null +++ b/src/speechify/core/force_multipart.py @@ -0,0 +1,18 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict + + +class ForceMultipartDict(Dict[str, Any]): + """ + A dictionary subclass that always evaluates to True in boolean contexts. + + This is used to force multipart/form-data encoding in HTTP requests even when + the dictionary is empty, which would normally evaluate to False. + """ + + def __bool__(self) -> bool: + return True + + +FORCE_MULTIPART = ForceMultipartDict() diff --git a/src/speechify/core/http_client.py b/src/speechify/core/http_client.py index 275a54c..f686c57 100644 --- a/src/speechify/core/http_client.py +++ b/src/speechify/core/http_client.py @@ -2,25 +2,25 @@ import asyncio import email.utils -import json import re import time import typing -import urllib.parse from contextlib import asynccontextmanager, contextmanager from random import random import httpx - from .file import File, convert_file_dict_to_httpx_tuples +from .force_multipart import FORCE_MULTIPART from .jsonable_encoder import jsonable_encoder +from .logging import LogConfig, Logger, create_logger from .query_encoder import encode_query -from .remove_none_from_dict import remove_none_from_dict +from .remove_none_from_dict import remove_none_from_dict as remove_none_from_dict from .request_options import RequestOptions +from httpx._types import RequestFiles -INITIAL_RETRY_DELAY_SECONDS = 0.5 -MAX_RETRY_DELAY_SECONDS = 10 -MAX_RETRY_DELAY_SECONDS_FROM_HEADER = 30 +INITIAL_RETRY_DELAY_SECONDS = 1.0 +MAX_RETRY_DELAY_SECONDS = 60.0 +JITTER_FACTOR = 0.2 # 20% random jitter def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float]: @@ -64,6 +64,38 @@ def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float return seconds +def _add_positive_jitter(delay: float) -> float: + """Add positive jitter (0-20%) to prevent thundering herd.""" + jitter_multiplier = 1 + random() * JITTER_FACTOR + return delay * jitter_multiplier + + +def _add_symmetric_jitter(delay: float) -> float: + """Add symmetric jitter (±10%) for exponential backoff.""" + jitter_multiplier = 1 + (random() - 0.5) * JITTER_FACTOR + return delay * jitter_multiplier + + +def _parse_x_ratelimit_reset(response_headers: httpx.Headers) -> typing.Optional[float]: + """ + Parse the X-RateLimit-Reset header (Unix timestamp in seconds). + Returns seconds to wait, or None if header is missing/invalid. + """ + reset_time_str = response_headers.get("x-ratelimit-reset") + if reset_time_str is None: + return None + + try: + reset_time = int(reset_time_str) + delay = reset_time - time.time() + if delay > 0: + return delay + except (ValueError, TypeError): + pass + + return None + + def _retry_timeout(response: httpx.Response, retries: int) -> float: """ Determine the amount of time to wait before retrying a request. @@ -71,22 +103,94 @@ def _retry_timeout(response: httpx.Response, retries: int) -> float: with a jitter to determine the number of seconds to wait. """ - # If the API asks us to wait a certain amount of time (and it's a reasonable amount), just do what it says. + # 1. Check Retry-After header first retry_after = _parse_retry_after(response.headers) - if retry_after is not None and retry_after <= MAX_RETRY_DELAY_SECONDS_FROM_HEADER: - return retry_after + if retry_after is not None and retry_after > 0: + return min(retry_after, MAX_RETRY_DELAY_SECONDS) + + # 2. Check X-RateLimit-Reset header (with positive jitter) + ratelimit_reset = _parse_x_ratelimit_reset(response.headers) + if ratelimit_reset is not None: + return _add_positive_jitter(min(ratelimit_reset, MAX_RETRY_DELAY_SECONDS)) - # Apply exponential backoff, capped at MAX_RETRY_DELAY_SECONDS. - retry_delay = min(INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS) + # 3. Fall back to exponential backoff (with symmetric jitter) + backoff = min(INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS) + return _add_symmetric_jitter(backoff) - # Add a randomness / jitter to the retry delay to avoid overwhelming the server with retries. - timeout = retry_delay * (1 - 0.25 * random()) - return timeout if timeout >= 0 else 0 + +def _retry_timeout_from_retries(retries: int) -> float: + """Determine retry timeout using exponential backoff when no response is available.""" + backoff = min(INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS) + return _add_symmetric_jitter(backoff) def _should_retry(response: httpx.Response) -> bool: - retryable_400s = [429, 408, 409] - return response.status_code >= 500 or response.status_code in retryable_400s + return response.status_code >= 500 or response.status_code in [429, 408, 409] + + +_SENSITIVE_HEADERS = frozenset( + { + "authorization", + "www-authenticate", + "x-api-key", + "api-key", + "apikey", + "x-api-token", + "x-auth-token", + "auth-token", + "cookie", + "set-cookie", + "proxy-authorization", + "proxy-authenticate", + "x-csrf-token", + "x-xsrf-token", + "x-session-token", + "x-access-token", + } +) + + +def _redact_headers(headers: typing.Dict[str, str]) -> typing.Dict[str, str]: + return {k: ("[REDACTED]" if k.lower() in _SENSITIVE_HEADERS else v) for k, v in headers.items()} + + +def _build_url(base_url: str, path: typing.Optional[str]) -> str: + """ + Build a full URL by joining a base URL with a path. + + This function correctly handles base URLs that contain path prefixes (e.g., tenant-based URLs) + by using string concatenation instead of urllib.parse.urljoin(), which would incorrectly + strip path components when the path starts with '/'. + + Example: + >>> _build_url("https://cloud.example.com/org/tenant/api", "/users") + 'https://cloud.example.com/org/tenant/api/users' + + Args: + base_url: The base URL, which may contain path prefixes. + path: The path to append. Can be None or empty string. + + Returns: + The full URL with base_url and path properly joined. + """ + if not path: + return base_url + return f"{base_url.rstrip('/')}/{path.lstrip('/')}" + + +def _maybe_filter_none_from_multipart_data( + data: typing.Optional[typing.Any], + request_files: typing.Optional[RequestFiles], + force_multipart: typing.Optional[bool], +) -> typing.Optional[typing.Any]: + """ + Filter None values from data body for multipart/form requests. + This prevents httpx from converting None to empty strings in multipart encoding. + Only applies when files are present or force_multipart is True. + """ + if data is not None and isinstance(data, typing.Mapping) and (request_files or force_multipart): + return remove_none_from_dict(data) + return data def remove_omit_from_dict( @@ -143,8 +247,19 @@ def get_request_body( # If both data and json are None, we send json data in the event extra properties are specified json_body = maybe_filter_request_body(json, request_options, omit) - # If you have an empty JSON body, you should just send None - return (json_body if json_body != {} else None), data_body if data_body != {} else None + has_additional_body_parameters = bool( + request_options is not None and request_options.get("additional_body_parameters") + ) + + # Only collapse empty dict to None when the body was not explicitly provided + # and there are no additional body parameters. This preserves explicit empty + # bodies (e.g., when an endpoint has a request body type but all fields are optional). + if json_body == {} and json is None and not has_additional_body_parameters: + json_body = None + if data_body == {} and data is None and not has_additional_body_parameters: + data_body = None + + return json_body, data_body class HttpClient: @@ -155,11 +270,15 @@ def __init__( base_timeout: typing.Callable[[], typing.Optional[float]], base_headers: typing.Callable[[], typing.Dict[str, str]], base_url: typing.Optional[typing.Callable[[], str]] = None, + base_max_retries: int = 2, + logging_config: typing.Optional[typing.Union[LogConfig, Logger]] = None, ): self.base_url = base_url self.base_timeout = base_timeout self.base_headers = base_headers + self.base_max_retries = base_max_retries self.httpx_client = httpx_client + self.logger = create_logger(logging_config) def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str: base_url = maybe_base_url @@ -180,11 +299,17 @@ def request( json: typing.Optional[typing.Any] = None, data: typing.Optional[typing.Any] = None, content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, - files: typing.Optional[typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, headers: typing.Optional[typing.Dict[str, typing.Any]] = None, request_options: typing.Optional[RequestOptions] = None, - retries: int = 2, + retries: int = 0, omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, ) -> httpx.Response: base_url = self.get_base_url(base_url) timeout = ( @@ -195,49 +320,97 @@ def request( json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) - response = self.httpx_client.request( - method=method, - url=urllib.parse.urljoin(f"{base_url}/", path), - headers=jsonable_encoder( + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + + data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart) + + # Compute encoded params separately to avoid passing empty list to httpx + # (httpx strips existing query params from URL when params=[] is passed) + _encoded_params = encode_query( + jsonable_encoder( remove_none_from_dict( - { - **self.base_headers(), - **(headers if headers is not None else {}), - **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}), - } - ) - ), - params=encode_query( - jsonable_encoder( - remove_none_from_dict( - remove_omit_from_dict( - { - **(params if params is not None else {}), - **( - request_options.get("additional_query_parameters", {}) or {} - if request_options is not None - else {} - ), - }, - omit, - ) + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) or {} + if request_options is not None + else {} + ), + }, + omit, ) ) - ), - json=json_body, - data=data_body, - content=content, - files=( - convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) - if (files is not None and files is not omit) - else None - ), - timeout=timeout, + ) + ) + + _request_url = _build_url(base_url, path) + _request_headers = jsonable_encoder( + remove_none_from_dict( + { + **self.base_headers(), + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}), + } + ) + ) + + if self.logger.is_debug(): + self.logger.debug( + "Making HTTP request", + method=method, + url=_request_url, + headers=_redact_headers(_request_headers), + has_body=json_body is not None or data_body is not None, + ) + + max_retries: int = ( + request_options.get("max_retries", self.base_max_retries) + if request_options is not None + else self.base_max_retries ) - max_retries: int = request_options.get("max_retries", 0) if request_options is not None else 0 + try: + response = self.httpx_client.request( + method=method, + url=_request_url, + headers=_request_headers, + params=_encoded_params if _encoded_params else None, + json=json_body, + data=data_body, + content=content, + files=request_files, + timeout=timeout, + ) + except (httpx.ConnectError, httpx.RemoteProtocolError): + if retries < max_retries: + time.sleep(_retry_timeout_from_retries(retries=retries)) + return self.request( + path=path, + method=method, + base_url=base_url, + params=params, + json=json, + data=data, + content=content, + files=files, + headers=headers, + request_options=request_options, + retries=retries + 1, + omit=omit, + force_multipart=force_multipart, + ) + raise + if _should_retry(response=response): - if max_retries > retries: + if retries < max_retries: time.sleep(_retry_timeout(response=response, retries=retries)) return self.request( path=path, @@ -245,12 +418,32 @@ def request( base_url=base_url, params=params, json=json, + data=data, content=content, files=files, headers=headers, request_options=request_options, retries=retries + 1, omit=omit, + force_multipart=force_multipart, + ) + + if self.logger.is_debug(): + if 200 <= response.status_code < 400: + self.logger.debug( + "HTTP request succeeded", + method=method, + url=_request_url, + status_code=response.status_code, + ) + + if self.logger.is_error(): + if response.status_code >= 400: + self.logger.error( + "HTTP request failed with error status", + method=method, + url=_request_url, + status_code=response.status_code, ) return response @@ -266,11 +459,17 @@ def stream( json: typing.Optional[typing.Any] = None, data: typing.Optional[typing.Any] = None, content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, - files: typing.Optional[typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, headers: typing.Optional[typing.Dict[str, typing.Any]] = None, request_options: typing.Optional[RequestOptions] = None, - retries: int = 2, + retries: int = 0, omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, ) -> typing.Iterator[httpx.Response]: base_url = self.get_base_url(base_url) timeout = ( @@ -279,45 +478,67 @@ def stream( else self.base_timeout() ) + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) - with self.httpx_client.stream( - method=method, - url=urllib.parse.urljoin(f"{base_url}/", path), - headers=jsonable_encoder( + data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart) + + # Compute encoded params separately to avoid passing empty list to httpx + # (httpx strips existing query params from URL when params=[] is passed) + _encoded_params = encode_query( + jsonable_encoder( remove_none_from_dict( - { - **self.base_headers(), - **(headers if headers is not None else {}), - **(request_options.get("additional_headers", {}) if request_options is not None else {}), - } - ) - ), - params=encode_query( - jsonable_encoder( - remove_none_from_dict( - remove_omit_from_dict( - { - **(params if params is not None else {}), - **( - request_options.get("additional_query_parameters", {}) - if request_options is not None - else {} - ), - }, - omit, - ) + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) + if request_options is not None + else {} + ), + }, + omit, ) ) - ), + ) + ) + + _request_url = _build_url(base_url, path) + _request_headers = jsonable_encoder( + remove_none_from_dict( + { + **self.base_headers(), + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) if request_options is not None else {}), + } + ) + ) + + if self.logger.is_debug(): + self.logger.debug( + "Making streaming HTTP request", + method=method, + url=_request_url, + headers=_redact_headers(_request_headers), + ) + + with self.httpx_client.stream( + method=method, + url=_request_url, + headers=_request_headers, + params=_encoded_params if _encoded_params else None, json=json_body, data=data_body, content=content, - files=( - convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) - if (files is not None and files is not omit) - else None - ), + files=request_files, timeout=timeout, ) as stream: yield stream @@ -331,11 +552,22 @@ def __init__( base_timeout: typing.Callable[[], typing.Optional[float]], base_headers: typing.Callable[[], typing.Dict[str, str]], base_url: typing.Optional[typing.Callable[[], str]] = None, + base_max_retries: int = 2, + async_base_headers: typing.Optional[typing.Callable[[], typing.Awaitable[typing.Dict[str, str]]]] = None, + logging_config: typing.Optional[typing.Union[LogConfig, Logger]] = None, ): self.base_url = base_url self.base_timeout = base_timeout self.base_headers = base_headers + self.base_max_retries = base_max_retries + self.async_base_headers = async_base_headers self.httpx_client = httpx_client + self.logger = create_logger(logging_config) + + async def _get_headers(self) -> typing.Dict[str, str]: + if self.async_base_headers is not None: + return await self.async_base_headers() + return self.base_headers() def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str: base_url = maybe_base_url @@ -356,11 +588,17 @@ async def request( json: typing.Optional[typing.Any] = None, data: typing.Optional[typing.Any] = None, content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, - files: typing.Optional[typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, headers: typing.Optional[typing.Dict[str, typing.Any]] = None, request_options: typing.Optional[RequestOptions] = None, - retries: int = 2, + retries: int = 0, omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, ) -> httpx.Response: base_url = self.get_base_url(base_url) timeout = ( @@ -369,52 +607,102 @@ async def request( else self.base_timeout() ) + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) - # Add the input to each of these and do None-safety checks - response = await self.httpx_client.request( - method=method, - url=urllib.parse.urljoin(f"{base_url}/", path), - headers=jsonable_encoder( + data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart) + + # Get headers (supports async token providers) + _headers = await self._get_headers() + + # Compute encoded params separately to avoid passing empty list to httpx + # (httpx strips existing query params from URL when params=[] is passed) + _encoded_params = encode_query( + jsonable_encoder( remove_none_from_dict( - { - **self.base_headers(), - **(headers if headers is not None else {}), - **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}), - } - ) - ), - params=encode_query( - jsonable_encoder( - remove_none_from_dict( - remove_omit_from_dict( - { - **(params if params is not None else {}), - **( - request_options.get("additional_query_parameters", {}) or {} - if request_options is not None - else {} - ), - }, - omit, - ) + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) or {} + if request_options is not None + else {} + ), + }, + omit, ) ) - ), - json=json_body, - data=data_body, - content=content, - files=( - convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) - if files is not None - else None - ), - timeout=timeout, + ) ) - max_retries: int = request_options.get("max_retries", 0) if request_options is not None else 0 + _request_url = _build_url(base_url, path) + _request_headers = jsonable_encoder( + remove_none_from_dict( + { + **_headers, + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}), + } + ) + ) + + if self.logger.is_debug(): + self.logger.debug( + "Making HTTP request", + method=method, + url=_request_url, + headers=_redact_headers(_request_headers), + has_body=json_body is not None or data_body is not None, + ) + + max_retries: int = ( + request_options.get("max_retries", self.base_max_retries) + if request_options is not None + else self.base_max_retries + ) + + try: + response = await self.httpx_client.request( + method=method, + url=_request_url, + headers=_request_headers, + params=_encoded_params if _encoded_params else None, + json=json_body, + data=data_body, + content=content, + files=request_files, + timeout=timeout, + ) + except (httpx.ConnectError, httpx.RemoteProtocolError): + if retries < max_retries: + await asyncio.sleep(_retry_timeout_from_retries(retries=retries)) + return await self.request( + path=path, + method=method, + base_url=base_url, + params=params, + json=json, + data=data, + content=content, + files=files, + headers=headers, + request_options=request_options, + retries=retries + 1, + omit=omit, + force_multipart=force_multipart, + ) + raise + if _should_retry(response=response): - if max_retries > retries: + if retries < max_retries: await asyncio.sleep(_retry_timeout(response=response, retries=retries)) return await self.request( path=path, @@ -422,13 +710,34 @@ async def request( base_url=base_url, params=params, json=json, + data=data, content=content, files=files, headers=headers, request_options=request_options, retries=retries + 1, omit=omit, + force_multipart=force_multipart, ) + + if self.logger.is_debug(): + if 200 <= response.status_code < 400: + self.logger.debug( + "HTTP request succeeded", + method=method, + url=_request_url, + status_code=response.status_code, + ) + + if self.logger.is_error(): + if response.status_code >= 400: + self.logger.error( + "HTTP request failed with error status", + method=method, + url=_request_url, + status_code=response.status_code, + ) + return response @asynccontextmanager @@ -442,11 +751,17 @@ async def stream( json: typing.Optional[typing.Any] = None, data: typing.Optional[typing.Any] = None, content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, - files: typing.Optional[typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, headers: typing.Optional[typing.Dict[str, typing.Any]] = None, request_options: typing.Optional[RequestOptions] = None, - retries: int = 2, + retries: int = 0, omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, ) -> typing.AsyncIterator[httpx.Response]: base_url = self.get_base_url(base_url) timeout = ( @@ -455,45 +770,70 @@ async def stream( else self.base_timeout() ) + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) - async with self.httpx_client.stream( - method=method, - url=urllib.parse.urljoin(f"{base_url}/", path), - headers=jsonable_encoder( + data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart) + + # Get headers (supports async token providers) + _headers = await self._get_headers() + + # Compute encoded params separately to avoid passing empty list to httpx + # (httpx strips existing query params from URL when params=[] is passed) + _encoded_params = encode_query( + jsonable_encoder( remove_none_from_dict( - { - **self.base_headers(), - **(headers if headers is not None else {}), - **(request_options.get("additional_headers", {}) if request_options is not None else {}), - } - ) - ), - params=encode_query( - jsonable_encoder( - remove_none_from_dict( - remove_omit_from_dict( - { - **(params if params is not None else {}), - **( - request_options.get("additional_query_parameters", {}) - if request_options is not None - else {} - ), - }, - omit=omit, - ) + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) + if request_options is not None + else {} + ), + }, + omit=omit, ) ) - ), + ) + ) + + _request_url = _build_url(base_url, path) + _request_headers = jsonable_encoder( + remove_none_from_dict( + { + **_headers, + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) if request_options is not None else {}), + } + ) + ) + + if self.logger.is_debug(): + self.logger.debug( + "Making streaming HTTP request", + method=method, + url=_request_url, + headers=_redact_headers(_request_headers), + ) + + async with self.httpx_client.stream( + method=method, + url=_request_url, + headers=_request_headers, + params=_encoded_params if _encoded_params else None, json=json_body, data=data_body, content=content, - files=( - convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) - if files is not None - else None - ), + files=request_files, timeout=timeout, ) as stream: yield stream diff --git a/src/speechify/core/http_response.py b/src/speechify/core/http_response.py new file mode 100644 index 0000000..00bb109 --- /dev/null +++ b/src/speechify/core/http_response.py @@ -0,0 +1,59 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Dict, Generic, TypeVar + +import httpx + +# Generic to represent the underlying type of the data wrapped by the HTTP response. +T = TypeVar("T") + + +class BaseHttpResponse: + """Minimalist HTTP response wrapper that exposes response headers and status code.""" + + _response: httpx.Response + + def __init__(self, response: httpx.Response): + self._response = response + + @property + def headers(self) -> Dict[str, str]: + return dict(self._response.headers) + + @property + def status_code(self) -> int: + return self._response.status_code + + +class HttpResponse(Generic[T], BaseHttpResponse): + """HTTP response wrapper that exposes response headers and data.""" + + _data: T + + def __init__(self, response: httpx.Response, data: T): + super().__init__(response) + self._data = data + + @property + def data(self) -> T: + return self._data + + def close(self) -> None: + self._response.close() + + +class AsyncHttpResponse(Generic[T], BaseHttpResponse): + """HTTP response wrapper that exposes response headers and data.""" + + _data: T + + def __init__(self, response: httpx.Response, data: T): + super().__init__(response) + self._data = data + + @property + def data(self) -> T: + return self._data + + async def close(self) -> None: + await self._response.aclose() diff --git a/src/speechify/core/http_sse/__init__.py b/src/speechify/core/http_sse/__init__.py new file mode 100644 index 0000000..730e5a3 --- /dev/null +++ b/src/speechify/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/src/speechify/core/http_sse/_api.py b/src/speechify/core/http_sse/_api.py new file mode 100644 index 0000000..fd13730 --- /dev/null +++ b/src/speechify/core/http_sse/_api.py @@ -0,0 +1,170 @@ +# This file was auto-generated by Fern from our API Definition. + +import codecs +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + +MAX_LINE_SIZE: int = 1_048_576 # 1 MiB + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + @staticmethod + def _normalize_sse_line_endings(buf: str) -> str: + """Normalize line endings per the SSE spec (\\r\\n → \\n, bare \\r → \\n). + + A trailing \\r is preserved because it may pair with a leading \\n in + the next chunk to form a single \\r\\n terminator. + """ + buf = buf.replace("\r\n", "\n") + if buf.endswith("\r"): + return buf[:-1].replace("\r", "\n") + "\r" + return buf.replace("\r", "\n") + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + text_decoder = codecs.getincrementaldecoder(charset)(errors="replace") + + buf = "" + for chunk in self._response.iter_bytes(): + buf += text_decoder.decode(chunk) + buf = self._normalize_sse_line_endings(buf) + + while "\n" in buf: + line, buf = buf.split("\n", 1) + sse = decoder.decode(line) + if sse is not None: + yield sse + + if len(buf) > MAX_LINE_SIZE: + raise SSEError( + f"SSE line exceeded maximum size of {MAX_LINE_SIZE} characters without encountering a newline" + ) + + # Flush any remaining bytes from the incremental decoder + buf += text_decoder.decode(b"", final=True) + buf = buf.replace("\r\n", "\n").replace("\r", "\n") + + if len(buf) > MAX_LINE_SIZE: + raise SSEError( + f"SSE line exceeded maximum size of {MAX_LINE_SIZE} characters without encountering a newline" + ) + + while "\n" in buf: + line, buf = buf.split("\n", 1) + sse = decoder.decode(line) + if sse is not None: + yield sse + + if buf.strip(): + sse = decoder.decode(buf) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + text_decoder = codecs.getincrementaldecoder(charset)(errors="replace") + + buf = "" + async for chunk in self._response.aiter_bytes(): + buf += text_decoder.decode(chunk) + buf = self._normalize_sse_line_endings(buf) + + while "\n" in buf: + line, buf = buf.split("\n", 1) + sse = decoder.decode(line) + if sse is not None: + yield sse + + if len(buf) > MAX_LINE_SIZE: + raise SSEError( + f"SSE line exceeded maximum size of {MAX_LINE_SIZE} characters without encountering a newline" + ) + + # Flush any remaining bytes from the incremental decoder + buf += text_decoder.decode(b"", final=True) + buf = buf.replace("\r\n", "\n").replace("\r", "\n") + + if len(buf) > MAX_LINE_SIZE: + raise SSEError( + f"SSE line exceeded maximum size of {MAX_LINE_SIZE} characters without encountering a newline" + ) + + while "\n" in buf: + line, buf = buf.split("\n", 1) + sse = decoder.decode(line) + if sse is not None: + yield sse + + if buf.strip(): + sse = decoder.decode(buf) + if sse is not None: + yield sse + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/src/speechify/core/http_sse/_decoders.py b/src/speechify/core/http_sse/_decoders.py new file mode 100644 index 0000000..339b089 --- /dev/null +++ b/src/speechify/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/src/speechify/tts/outbound_calls/__init__.py b/src/speechify/core/http_sse/_exceptions.py similarity index 51% rename from src/speechify/tts/outbound_calls/__init__.py rename to src/speechify/core/http_sse/_exceptions.py index f3ea265..81605a8 100644 --- a/src/speechify/tts/outbound_calls/__init__.py +++ b/src/speechify/core/http_sse/_exceptions.py @@ -1,2 +1,7 @@ # This file was auto-generated by Fern from our API Definition. +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/src/speechify/core/http_sse/_models.py b/src/speechify/core/http_sse/_models.py new file mode 100644 index 0000000..1af57f8 --- /dev/null +++ b/src/speechify/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/src/speechify/core/jsonable_encoder.py b/src/speechify/core/jsonable_encoder.py index 1b631e9..5b0902e 100644 --- a/src/speechify/core/jsonable_encoder.py +++ b/src/speechify/core/jsonable_encoder.py @@ -17,7 +17,6 @@ from typing import Any, Callable, Dict, List, Optional, Set, Union import pydantic - from .datetime_utils import serialize_datetime from .pydantic_utilities import ( IS_PYDANTIC_V2, @@ -31,6 +30,10 @@ def jsonable_encoder(obj: Any, custom_encoder: Optional[Dict[Any, Callable[[Any], Any]]] = None) -> Any: custom_encoder = custom_encoder or {} + # Generated SDKs use Ellipsis (`...`) as the sentinel value for "OMIT". + # OMIT values should be excluded from serialized payloads. + if obj is Ellipsis: + return None if custom_encoder: if type(obj) in custom_encoder: return custom_encoder[type(obj)](obj) @@ -71,6 +74,8 @@ def jsonable_encoder(obj: Any, custom_encoder: Optional[Dict[Any, Callable[[Any] allowed_keys = set(obj.keys()) for key, value in obj.items(): if key in allowed_keys: + if value is Ellipsis: + continue encoded_key = jsonable_encoder(key, custom_encoder=custom_encoder) encoded_value = jsonable_encoder(value, custom_encoder=custom_encoder) encoded_dict[encoded_key] = encoded_value @@ -78,6 +83,8 @@ def jsonable_encoder(obj: Any, custom_encoder: Optional[Dict[Any, Callable[[Any] if isinstance(obj, (list, set, frozenset, GeneratorType, tuple)): encoded_list = [] for item in obj: + if item is Ellipsis: + continue encoded_list.append(jsonable_encoder(item, custom_encoder=custom_encoder)) return encoded_list @@ -99,3 +106,15 @@ def fallback_serializer(o: Any) -> Any: return jsonable_encoder(data, custom_encoder=custom_encoder) return to_jsonable_with_fallback(obj, fallback_serializer) + + +def encode_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment. + + Ensures proper string conversion for all types, including + booleans which need lowercase 'true'/'false' rather than + Python's 'True'/'False'. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return str(jsonable_encoder(obj)) diff --git a/src/speechify/core/logging.py b/src/speechify/core/logging.py new file mode 100644 index 0000000..e5e5724 --- /dev/null +++ b/src/speechify/core/logging.py @@ -0,0 +1,107 @@ +# This file was auto-generated by Fern from our API Definition. + +import logging +import typing + +LogLevel = typing.Literal["debug", "info", "warn", "error"] + +_LOG_LEVEL_MAP: typing.Dict[LogLevel, int] = { + "debug": 1, + "info": 2, + "warn": 3, + "error": 4, +} + + +class ILogger(typing.Protocol): + def debug(self, message: str, **kwargs: typing.Any) -> None: ... + def info(self, message: str, **kwargs: typing.Any) -> None: ... + def warn(self, message: str, **kwargs: typing.Any) -> None: ... + def error(self, message: str, **kwargs: typing.Any) -> None: ... + + +class ConsoleLogger: + _logger: logging.Logger + + def __init__(self) -> None: + self._logger = logging.getLogger("fern") + if not self._logger.handlers: + handler = logging.StreamHandler() + handler.setFormatter(logging.Formatter("%(levelname)s - %(message)s")) + self._logger.addHandler(handler) + self._logger.setLevel(logging.DEBUG) + + def debug(self, message: str, **kwargs: typing.Any) -> None: + self._logger.debug(message, extra=kwargs) + + def info(self, message: str, **kwargs: typing.Any) -> None: + self._logger.info(message, extra=kwargs) + + def warn(self, message: str, **kwargs: typing.Any) -> None: + self._logger.warning(message, extra=kwargs) + + def error(self, message: str, **kwargs: typing.Any) -> None: + self._logger.error(message, extra=kwargs) + + +class LogConfig(typing.TypedDict, total=False): + level: LogLevel + logger: ILogger + silent: bool + + +class Logger: + _level: int + _logger: ILogger + _silent: bool + + def __init__(self, *, level: LogLevel, logger: ILogger, silent: bool) -> None: + self._level = _LOG_LEVEL_MAP[level] + self._logger = logger + self._silent = silent + + def _should_log(self, level: LogLevel) -> bool: + return not self._silent and self._level <= _LOG_LEVEL_MAP[level] + + def is_debug(self) -> bool: + return self._should_log("debug") + + def is_info(self) -> bool: + return self._should_log("info") + + def is_warn(self) -> bool: + return self._should_log("warn") + + def is_error(self) -> bool: + return self._should_log("error") + + def debug(self, message: str, **kwargs: typing.Any) -> None: + if self.is_debug(): + self._logger.debug(message, **kwargs) + + def info(self, message: str, **kwargs: typing.Any) -> None: + if self.is_info(): + self._logger.info(message, **kwargs) + + def warn(self, message: str, **kwargs: typing.Any) -> None: + if self.is_warn(): + self._logger.warn(message, **kwargs) + + def error(self, message: str, **kwargs: typing.Any) -> None: + if self.is_error(): + self._logger.error(message, **kwargs) + + +_default_logger: Logger = Logger(level="info", logger=ConsoleLogger(), silent=True) + + +def create_logger(config: typing.Optional[typing.Union[LogConfig, Logger]] = None) -> Logger: + if config is None: + return _default_logger + if isinstance(config, Logger): + return config + return Logger( + level=config.get("level", "info"), + logger=config.get("logger", ConsoleLogger()), + silent=config.get("silent", True), + ) diff --git a/src/speechify/core/parse_error.py b/src/speechify/core/parse_error.py new file mode 100644 index 0000000..4527c6a --- /dev/null +++ b/src/speechify/core/parse_error.py @@ -0,0 +1,36 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict, Optional + + +class ParsingError(Exception): + """ + Raised when the SDK fails to parse/validate a response from the server. + This typically indicates that the server returned a response whose shape + does not match the expected schema. + """ + + headers: Optional[Dict[str, str]] + status_code: Optional[int] + body: Any + cause: Optional[Exception] + + def __init__( + self, + *, + headers: Optional[Dict[str, str]] = None, + status_code: Optional[int] = None, + body: Any = None, + cause: Optional[Exception] = None, + ) -> None: + self.headers = headers + self.status_code = status_code + self.body = body + self.cause = cause + super().__init__() + if cause is not None: + self.__cause__ = cause + + def __str__(self) -> str: + cause_str = f", cause: {self.cause}" if self.cause is not None else "" + return f"headers: {self.headers}, status_code: {self.status_code}, body: {self.body}{cause_str}" diff --git a/src/speechify/core/pydantic_utilities.py b/src/speechify/core/pydantic_utilities.py index ca1f479..6587f5e 100644 --- a/src/speechify/core/pydantic_utilities.py +++ b/src/speechify/core/pydantic_utilities.py @@ -2,90 +2,280 @@ # nopycln: file import datetime as dt -import typing +import inspect +import json +import logging from collections import defaultdict +from dataclasses import asdict +from typing import ( + TYPE_CHECKING, + Any, + Callable, + ClassVar, + Dict, + List, + Mapping, + Optional, + Set, + Tuple, + Type, + TypeVar, + Union, + cast, +) +import pydantic import typing_extensions +from pydantic.fields import FieldInfo as _FieldInfo -import pydantic +_logger = logging.getLogger(__name__) -from .datetime_utils import serialize_datetime -from .serialization import convert_and_respect_annotation_metadata +if TYPE_CHECKING: + from .http_sse._models import ServerSentEvent IS_PYDANTIC_V2 = pydantic.VERSION.startswith("2.") if IS_PYDANTIC_V2: - # isort will try to reformat the comments on these imports, which breaks mypy - # isort: off - from pydantic.v1.datetime_parse import ( # type: ignore # pyright: ignore[reportMissingImports] # Pydantic v2 - parse_date as parse_date, + _datetime_adapter = pydantic.TypeAdapter(dt.datetime) # type: ignore[attr-defined] + _date_adapter = pydantic.TypeAdapter(dt.date) # type: ignore[attr-defined] + + def parse_datetime(value: Any) -> dt.datetime: # type: ignore[misc] + if isinstance(value, dt.datetime): + return value + return _datetime_adapter.validate_python(value) + + def parse_date(value: Any) -> dt.date: # type: ignore[misc] + if isinstance(value, dt.datetime): + return value.date() + if isinstance(value, dt.date): + return value + return _date_adapter.validate_python(value) + + # Avoid importing from pydantic.v1 to maintain Python 3.14 compatibility. + from typing import get_args as get_args # type: ignore[assignment] + from typing import get_origin as get_origin # type: ignore[assignment] + + def is_literal_type(tp: Optional[Type[Any]]) -> bool: # type: ignore[misc] + return typing_extensions.get_origin(tp) is typing_extensions.Literal + + def is_union(tp: Optional[Type[Any]]) -> bool: # type: ignore[misc] + return tp is Union or typing_extensions.get_origin(tp) is Union # type: ignore[comparison-overlap] + + # Inline encoders_by_type to avoid importing from pydantic.v1.json + import re as _re + from collections import deque as _deque + from decimal import Decimal as _Decimal + from enum import Enum as _Enum + from ipaddress import ( + IPv4Address as _IPv4Address, ) - from pydantic.v1.datetime_parse import ( # pyright: ignore[reportMissingImports] # Pydantic v2 - parse_datetime as parse_datetime, + from ipaddress import ( + IPv4Interface as _IPv4Interface, ) - from pydantic.v1.json import ( # type: ignore # pyright: ignore[reportMissingImports] # Pydantic v2 - ENCODERS_BY_TYPE as encoders_by_type, + from ipaddress import ( + IPv4Network as _IPv4Network, ) - from pydantic.v1.typing import ( # type: ignore # pyright: ignore[reportMissingImports] # Pydantic v2 - get_args as get_args, + from ipaddress import ( + IPv6Address as _IPv6Address, ) - from pydantic.v1.typing import ( # pyright: ignore[reportMissingImports] # Pydantic v2 - get_origin as get_origin, + from ipaddress import ( + IPv6Interface as _IPv6Interface, ) - from pydantic.v1.typing import ( # pyright: ignore[reportMissingImports] # Pydantic v2 - is_literal_type as is_literal_type, + from ipaddress import ( + IPv6Network as _IPv6Network, ) - from pydantic.v1.typing import ( # pyright: ignore[reportMissingImports] # Pydantic v2 - is_union as is_union, - ) - from pydantic.v1.fields import ModelField as ModelField # type: ignore # pyright: ignore[reportMissingImports] # Pydantic v2 + from pathlib import Path as _Path + from types import GeneratorType as _GeneratorType + from uuid import UUID as _UUID + + from pydantic.fields import FieldInfo as ModelField # type: ignore[no-redef, assignment] + + def _decimal_encoder(dec_value: Any) -> Any: + if dec_value.as_tuple().exponent >= 0: + return int(dec_value) + return float(dec_value) + + encoders_by_type: Dict[Type[Any], Callable[[Any], Any]] = { # type: ignore[no-redef] + bytes: lambda o: o.decode(), + dt.date: lambda o: o.isoformat(), + dt.datetime: lambda o: o.isoformat(), + dt.time: lambda o: o.isoformat(), + dt.timedelta: lambda td: td.total_seconds(), + _Decimal: _decimal_encoder, + _Enum: lambda o: o.value, + frozenset: list, + _deque: list, + _GeneratorType: list, + _IPv4Address: str, + _IPv4Interface: str, + _IPv4Network: str, + _IPv6Address: str, + _IPv6Interface: str, + _IPv6Network: str, + _Path: str, + _re.Pattern: lambda o: o.pattern, + set: list, + _UUID: str, + } else: - from pydantic.datetime_parse import parse_date as parse_date # type: ignore # Pydantic v1 - from pydantic.datetime_parse import parse_datetime as parse_datetime # type: ignore # Pydantic v1 - from pydantic.fields import ModelField as ModelField # type: ignore # Pydantic v1 - from pydantic.json import ENCODERS_BY_TYPE as encoders_by_type # type: ignore # Pydantic v1 - from pydantic.typing import get_args as get_args # type: ignore # Pydantic v1 - from pydantic.typing import get_origin as get_origin # type: ignore # Pydantic v1 - from pydantic.typing import is_literal_type as is_literal_type # type: ignore # Pydantic v1 - from pydantic.typing import is_union as is_union # type: ignore # Pydantic v1 + from pydantic.datetime_parse import parse_date as parse_date # type: ignore[no-redef] + from pydantic.datetime_parse import parse_datetime as parse_datetime # type: ignore[no-redef] + from pydantic.fields import ModelField as ModelField # type: ignore[attr-defined, no-redef, assignment] + from pydantic.json import ENCODERS_BY_TYPE as encoders_by_type # type: ignore[no-redef] + from pydantic.typing import get_args as get_args # type: ignore[no-redef] + from pydantic.typing import get_origin as get_origin # type: ignore[no-redef] + from pydantic.typing import is_literal_type as is_literal_type # type: ignore[no-redef, assignment] + from pydantic.typing import is_union as is_union # type: ignore[no-redef] - # isort: on +from .datetime_utils import serialize_datetime +from .serialization import convert_and_respect_annotation_metadata +from typing_extensions import TypeAlias + +T = TypeVar("T") +Model = TypeVar("Model", bound=pydantic.BaseModel) + + +def parse_sse_obj(sse: "ServerSentEvent", type_: Type[T]) -> T: + """ + Parse a ServerSentEvent into the appropriate type. + + This function handles data-level discrimination where the discriminator + (e.g., 'type') is inside the 'data' payload. It parses the SSE data field + as JSON and deserializes it into the target type. + + Note: Protocol-level discrimination (where the discriminator comes from + the SSE event: field) is handled at code-generation time and does not + use this function. + + Args: + sse: The ServerSentEvent object to parse + type_: The target type to deserialize into + + Returns: + The parsed object of type T + + Note: + This function is only available in SDK contexts where http_sse module exists. + """ + sse_event = asdict(sse) + data_value = sse_event.get("data") + if isinstance(data_value, str) and data_value: + try: + parsed_data = json.loads(data_value) + return parse_obj_as(type_, parsed_data) + except json.JSONDecodeError as e: + _logger.warning( + "Failed to parse SSE data field as JSON: %s, data: %s", + e, + data_value[:100] if len(data_value) > 100 else data_value, + ) + return parse_obj_as(type_, sse_event) -T = typing.TypeVar("T") -Model = typing.TypeVar("Model", bound=pydantic.BaseModel) +_type_adapter_cache: Dict[int, Any] = {} -def parse_obj_as(type_: typing.Type[T], object_: typing.Any) -> T: - dealiased_object = convert_and_respect_annotation_metadata(object_=object_, annotation=type_, direction="read") - if IS_PYDANTIC_V2: - adapter = pydantic.TypeAdapter(type_) # type: ignore # Pydantic v2 - return adapter.validate_python(dealiased_object) +def _get_type_adapter(type_: Type[Any]) -> Any: + key = id(type_) + adapter = _type_adapter_cache.get(key) + if adapter is None: + adapter = pydantic.TypeAdapter(type_) # type: ignore[attr-defined] + _type_adapter_cache[key] = adapter + return adapter + + +def parse_obj_as(type_: Type[T], object_: Any) -> T: + # convert_and_respect_annotation_metadata is required for TypedDict aliasing. + # + # For Pydantic models, whether we should pre-dealias depends on how the model encodes aliasing: + # - If the model uses real Pydantic aliases (pydantic.Field(alias=...)), then we must pass wire keys through + # unchanged so Pydantic can validate them. + # - If the model encodes aliasing only via FieldMetadata annotations, then we MUST pre-dealias because Pydantic + # will not recognize those aliases during validation. + if inspect.isclass(type_) and issubclass(type_, pydantic.BaseModel): + has_pydantic_aliases = False + if IS_PYDANTIC_V2: + for field_name, field_info in getattr(type_, "model_fields", {}).items(): # type: ignore[attr-defined] + alias = getattr(field_info, "alias", None) + if alias is not None and alias != field_name: + has_pydantic_aliases = True + break + else: + for field in getattr(type_, "__fields__", {}).values(): + alias = getattr(field, "alias", None) + name = getattr(field, "name", None) + if alias is not None and name is not None and alias != name: + has_pydantic_aliases = True + break + + dealiased_object = ( + object_ + if has_pydantic_aliases + else convert_and_respect_annotation_metadata(object_=object_, annotation=type_, direction="read") + ) else: - return pydantic.parse_obj_as(type_, dealiased_object) + dealiased_object = convert_and_respect_annotation_metadata(object_=object_, annotation=type_, direction="read") + if IS_PYDANTIC_V2: + adapter = _get_type_adapter(type_) + return adapter.validate_python(dealiased_object) # type: ignore[no-any-return] + return pydantic.parse_obj_as(type_, dealiased_object) -def to_jsonable_with_fallback( - obj: typing.Any, fallback_serializer: typing.Callable[[typing.Any], typing.Any] -) -> typing.Any: +def to_jsonable_with_fallback(obj: Any, fallback_serializer: Callable[[Any], Any]) -> Any: if IS_PYDANTIC_V2: from pydantic_core import to_jsonable_python return to_jsonable_python(obj, fallback=fallback_serializer) - else: - return fallback_serializer(obj) + return fallback_serializer(obj) class UniversalBaseModel(pydantic.BaseModel): if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + model_config: ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( # type: ignore[typeddict-unknown-key] # Allow fields beginning with `model_` to be used in the model protected_namespaces=(), - ) # type: ignore # Pydantic v2 - - @pydantic.model_serializer(mode="wrap", when_used="json") # type: ignore # Pydantic v2 - def serialize_model(self, handler: pydantic.SerializerFunctionWrapHandler) -> typing.Any: # type: ignore # Pydantic v2 - serialized = handler(self) + ) + + @pydantic.model_validator(mode="before") # type: ignore[attr-defined] + @classmethod + def _coerce_field_names_to_aliases(cls, data: Any) -> Any: + """ + Accept Python field names in input by rewriting them to their Pydantic aliases, + while avoiding silent collisions when a key could refer to multiple fields. + """ + if not isinstance(data, Mapping): + return data + + fields = getattr(cls, "model_fields", {}) # type: ignore[attr-defined] + name_to_alias: Dict[str, str] = {} + alias_to_name: Dict[str, str] = {} + + for name, field_info in fields.items(): + alias = getattr(field_info, "alias", None) or name + name_to_alias[name] = alias + if alias != name: + alias_to_name[alias] = name + + # Detect ambiguous keys: a key that is an alias for one field and a name for another. + ambiguous_keys = set(alias_to_name.keys()).intersection(set(name_to_alias.keys())) + for key in ambiguous_keys: + if key in data and name_to_alias[key] not in data: + raise ValueError( + f"Ambiguous input key '{key}': it is both a field name and an alias. " + "Provide the explicit alias key to disambiguate." + ) + + original_keys = set(data.keys()) + rewritten: Dict[str, Any] = dict(data) + for name, alias in name_to_alias.items(): + if alias != name and name in original_keys and alias not in rewritten: + rewritten[alias] = rewritten.pop(name) + + return rewritten + + @pydantic.model_serializer(mode="plain", when_used="json") # type: ignore[attr-defined] + def serialize_model(self) -> Any: # type: ignore[name-defined] + serialized = self.dict() # type: ignore[attr-defined] data = {k: serialize_datetime(v) if isinstance(v, dt.datetime) else v for k, v in serialized.items()} return data @@ -95,35 +285,63 @@ class Config: smart_union = True json_encoders = {dt.datetime: serialize_datetime} + @pydantic.root_validator(pre=True) + def _coerce_field_names_to_aliases(cls, values: Any) -> Any: + """ + Pydantic v1 equivalent of _coerce_field_names_to_aliases. + """ + if not isinstance(values, Mapping): + return values + + fields = getattr(cls, "__fields__", {}) + name_to_alias: Dict[str, str] = {} + alias_to_name: Dict[str, str] = {} + + for name, field in fields.items(): + alias = getattr(field, "alias", None) or name + name_to_alias[name] = alias + if alias != name: + alias_to_name[alias] = name + + ambiguous_keys = set(alias_to_name.keys()).intersection(set(name_to_alias.keys())) + for key in ambiguous_keys: + if key in values and name_to_alias[key] not in values: + raise ValueError( + f"Ambiguous input key '{key}': it is both a field name and an alias. " + "Provide the explicit alias key to disambiguate." + ) + + original_keys = set(values.keys()) + rewritten: Dict[str, Any] = dict(values) + for name, alias in name_to_alias.items(): + if alias != name and name in original_keys and alias not in rewritten: + rewritten[alias] = rewritten.pop(name) + + return rewritten + @classmethod - def model_construct( - cls: typing.Type["Model"], _fields_set: typing.Optional[typing.Set[str]] = None, **values: typing.Any - ) -> "Model": + def model_construct(cls: Type["Model"], _fields_set: Optional[Set[str]] = None, **values: Any) -> "Model": dealiased_object = convert_and_respect_annotation_metadata(object_=values, annotation=cls, direction="read") return cls.construct(_fields_set, **dealiased_object) @classmethod - def construct( - cls: typing.Type["Model"], _fields_set: typing.Optional[typing.Set[str]] = None, **values: typing.Any - ) -> "Model": + def construct(cls: Type["Model"], _fields_set: Optional[Set[str]] = None, **values: Any) -> "Model": dealiased_object = convert_and_respect_annotation_metadata(object_=values, annotation=cls, direction="read") if IS_PYDANTIC_V2: - return super().model_construct(_fields_set, **dealiased_object) # type: ignore # Pydantic v2 - else: - return super().construct(_fields_set, **dealiased_object) + return super().model_construct(_fields_set, **dealiased_object) # type: ignore[misc] + return super().construct(_fields_set, **dealiased_object) - def json(self, **kwargs: typing.Any) -> str: - kwargs_with_defaults: typing.Any = { + def json(self, **kwargs: Any) -> str: + kwargs_with_defaults = { "by_alias": True, "exclude_unset": True, **kwargs, } if IS_PYDANTIC_V2: - return super().model_dump_json(**kwargs_with_defaults) # type: ignore # Pydantic v2 - else: - return super().json(**kwargs_with_defaults) + return super().model_dump_json(**kwargs_with_defaults) # type: ignore[misc] + return super().json(**kwargs_with_defaults) - def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: + def dict(self, **kwargs: Any) -> Dict[str, Any]: """ Override the default dict method to `exclude_unset` by default. This function patches `exclude_unset` to work include fields within non-None default values. @@ -134,21 +352,21 @@ def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: # We'd ideally do the same for Pydantic V2, but it shells out to a library to serialize models # that we have less control over, and this is less intrusive than custom serializers for now. if IS_PYDANTIC_V2: - kwargs_with_defaults_exclude_unset: typing.Any = { + kwargs_with_defaults_exclude_unset = { **kwargs, "by_alias": True, "exclude_unset": True, "exclude_none": False, } - kwargs_with_defaults_exclude_none: typing.Any = { + kwargs_with_defaults_exclude_none = { **kwargs, "by_alias": True, "exclude_none": True, "exclude_unset": False, } dict_dump = deep_union_pydantic_dicts( - super().model_dump(**kwargs_with_defaults_exclude_unset), # type: ignore # Pydantic v2 - super().model_dump(**kwargs_with_defaults_exclude_none), # type: ignore # Pydantic v2 + super().model_dump(**kwargs_with_defaults_exclude_unset), # type: ignore[misc] + super().model_dump(**kwargs_with_defaults_exclude_none), # type: ignore[misc] ) else: @@ -168,7 +386,7 @@ def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: if default is not None: self.__fields_set__.add(name) - kwargs_with_defaults_exclude_unset_include_fields: typing.Any = { + kwargs_with_defaults_exclude_unset_include_fields = { "by_alias": True, "exclude_unset": True, "include": _fields_set, @@ -177,15 +395,16 @@ def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: dict_dump = super().dict(**kwargs_with_defaults_exclude_unset_include_fields) - return convert_and_respect_annotation_metadata(object_=dict_dump, annotation=self.__class__, direction="write") + return cast( + Dict[str, Any], + convert_and_respect_annotation_metadata(object_=dict_dump, annotation=self.__class__, direction="write"), + ) -def _union_list_of_pydantic_dicts( - source: typing.List[typing.Any], destination: typing.List[typing.Any] -) -> typing.List[typing.Any]: - converted_list: typing.List[typing.Any] = [] +def _union_list_of_pydantic_dicts(source: List[Any], destination: List[Any]) -> List[Any]: + converted_list: List[Any] = [] for i, item in enumerate(source): - destination_value = destination[i] # type: ignore + destination_value = destination[i] if isinstance(item, dict): converted_list.append(deep_union_pydantic_dicts(item, destination_value)) elif isinstance(item, list): @@ -195,9 +414,7 @@ def _union_list_of_pydantic_dicts( return converted_list -def deep_union_pydantic_dicts( - source: typing.Dict[str, typing.Any], destination: typing.Dict[str, typing.Any] -) -> typing.Dict[str, typing.Any]: +def deep_union_pydantic_dicts(source: Dict[str, Any], destination: Dict[str, Any]) -> Dict[str, Any]: for key, value in source.items(): node = destination.setdefault(key, {}) if isinstance(value, dict): @@ -215,18 +432,16 @@ def deep_union_pydantic_dicts( if IS_PYDANTIC_V2: - class V2RootModel(UniversalBaseModel, pydantic.RootModel): # type: ignore # Pydantic v2 + class V2RootModel(UniversalBaseModel, pydantic.RootModel): # type: ignore[misc, name-defined, type-arg] pass - UniversalRootModel: typing_extensions.TypeAlias = V2RootModel # type: ignore + UniversalRootModel: TypeAlias = V2RootModel # type: ignore[misc] else: - UniversalRootModel: typing_extensions.TypeAlias = UniversalBaseModel # type: ignore + UniversalRootModel: TypeAlias = UniversalBaseModel # type: ignore[misc, no-redef] -def encode_by_type(o: typing.Any) -> typing.Any: - encoders_by_class_tuples: typing.Dict[typing.Callable[[typing.Any], typing.Any], typing.Tuple[typing.Any, ...]] = ( - defaultdict(tuple) - ) +def encode_by_type(o: Any) -> Any: + encoders_by_class_tuples: Dict[Callable[[Any], Any], Tuple[Any, ...]] = defaultdict(tuple) for type_, encoder in encoders_by_type.items(): encoders_by_class_tuples[encoder] += (type_,) @@ -237,54 +452,51 @@ def encode_by_type(o: typing.Any) -> typing.Any: return encoder(o) -def update_forward_refs(model: typing.Type["Model"], **localns: typing.Any) -> None: +def update_forward_refs(model: Type["Model"], **localns: Any) -> None: if IS_PYDANTIC_V2: - model.model_rebuild(raise_errors=False) # type: ignore # Pydantic v2 + model.model_rebuild(raise_errors=False) # type: ignore[attr-defined] else: model.update_forward_refs(**localns) # Mirrors Pydantic's internal typing -AnyCallable = typing.Callable[..., typing.Any] +AnyCallable = Callable[..., Any] def universal_root_validator( pre: bool = False, -) -> typing.Callable[[AnyCallable], AnyCallable]: +) -> Callable[[AnyCallable], AnyCallable]: def decorator(func: AnyCallable) -> AnyCallable: if IS_PYDANTIC_V2: - return pydantic.model_validator(mode="before" if pre else "after")(func) # type: ignore # Pydantic v2 - else: - return pydantic.root_validator(pre=pre)(func) # type: ignore # Pydantic v1 + # In Pydantic v2, for RootModel we always use "before" mode + # The custom validators transform the input value before the model is created + return cast(AnyCallable, pydantic.model_validator(mode="before")(func)) # type: ignore[attr-defined] + return cast(AnyCallable, pydantic.root_validator(pre=pre)(func)) # type: ignore[call-overload] return decorator -def universal_field_validator(field_name: str, pre: bool = False) -> typing.Callable[[AnyCallable], AnyCallable]: +def universal_field_validator(field_name: str, pre: bool = False) -> Callable[[AnyCallable], AnyCallable]: def decorator(func: AnyCallable) -> AnyCallable: if IS_PYDANTIC_V2: - return pydantic.field_validator(field_name, mode="before" if pre else "after")(func) # type: ignore # Pydantic v2 - else: - return pydantic.validator(field_name, pre=pre)(func) # type: ignore # Pydantic v1 + return cast(AnyCallable, pydantic.field_validator(field_name, mode="before" if pre else "after")(func)) # type: ignore[attr-defined] + return cast(AnyCallable, pydantic.validator(field_name, pre=pre)(func)) return decorator -PydanticField = typing.Union[ModelField, pydantic.fields.FieldInfo] +PydanticField = Union[ModelField, _FieldInfo] -def _get_model_fields( - model: typing.Type["Model"], -) -> typing.Mapping[str, PydanticField]: +def _get_model_fields(model: Type["Model"]) -> Mapping[str, PydanticField]: if IS_PYDANTIC_V2: - return model.model_fields # type: ignore # Pydantic v2 - else: - return model.__fields__ # type: ignore # Pydantic v1 + return cast(Mapping[str, PydanticField], model.model_fields) # type: ignore[attr-defined] + return cast(Mapping[str, PydanticField], model.__fields__) -def _get_field_default(field: PydanticField) -> typing.Any: +def _get_field_default(field: PydanticField) -> Any: try: - value = field.get_default() # type: ignore # Pydantic < v1.10.15 + value = field.get_default() # type: ignore[union-attr] except: value = field.default if IS_PYDANTIC_V2: diff --git a/src/speechify/core/serialization.py b/src/speechify/core/serialization.py index cb5dcbf..1d753e2 100644 --- a/src/speechify/core/serialization.py +++ b/src/speechify/core/serialization.py @@ -4,9 +4,8 @@ import inspect import typing -import typing_extensions - import pydantic +import typing_extensions class FieldMetadata: @@ -27,6 +26,75 @@ def __init__(self, *, alias: str) -> None: self.alias = alias +# Resolving type hints (typing.get_type_hints) is expensive because it eval/compiles +# forward-reference annotations. The result is constant for a given type, so we cache it. +# This is critical for hot paths like SSE event parsing, where the same (often large +# discriminated-union) type is converted on every single event. +_type_hints_cache: typing.Dict[typing.Any, typing.Dict[str, typing.Any]] = {} + + +def _get_cached_type_hints(expected_type: typing.Any) -> typing.Dict[str, typing.Any]: + try: + cached = _type_hints_cache.get(expected_type) + except TypeError: + # Unhashable type; resolve without caching. + return _resolve_type_hints(expected_type) + if cached is None: + cached = _resolve_type_hints(expected_type) + _type_hints_cache[expected_type] = cached + return cached + + +def _resolve_type_hints(expected_type: typing.Any) -> typing.Dict[str, typing.Any]: + try: + return typing_extensions.get_type_hints(expected_type, include_extras=True) + except NameError: + # The type contains a circular reference, so we use the __annotations__ attribute directly. + return getattr(expected_type, "__annotations__", {}) + + +# Whether convert_and_respect_annotation_metadata can possibly rewrite anything for a given +# annotation, i.e. whether any reachable model/TypedDict field carries a FieldMetadata alias. +# This is constant per type, so we cache it and use it to short-circuit the recursive walk. +_requires_conversion_cache: typing.Dict[typing.Any, bool] = {} + + +def _requires_conversion(type_: typing.Any) -> bool: + try: + cached = _requires_conversion_cache.get(type_) + except TypeError: + # Unhashable annotation; compute without caching. + return _compute_requires_conversion(type_, set()) + if cached is None: + cached = _compute_requires_conversion(type_, set()) + _requires_conversion_cache[type_] = cached + return cached + + +def _compute_requires_conversion(type_: typing.Any, seen: typing.Set[typing.Any]) -> bool: + clean_type = _remove_annotations(type_) + + try: + if clean_type in seen: + return False + seen = seen | {clean_type} + except TypeError: + # Unhashable type; skip cycle tracking (the type graph is finite in practice). + pass + + # Models / TypedDicts: a field alias here means we must dealias; otherwise recurse into fields. + if (inspect.isclass(clean_type) and issubclass(clean_type, pydantic.BaseModel)) or typing_extensions.is_typeddict( + clean_type + ): + annotations = _get_cached_type_hints(clean_type) + if _get_alias_to_field_name(annotations): + return True + return any(_compute_requires_conversion(hint, seen) for hint in annotations.values()) + + # Containers / unions: recurse into the type arguments (List/Set/Sequence/Dict/Union/etc.). + return any(_compute_requires_conversion(arg, seen) for arg in typing_extensions.get_args(clean_type)) + + def convert_and_respect_annotation_metadata( *, object_: typing.Any, @@ -58,6 +126,13 @@ def convert_and_respect_annotation_metadata( return None if inner_type is None: inner_type = annotation + # The only thing this function ever rewrites is keys that carry a FieldMetadata + # alias. If nothing in the (cached) type graph has such an alias, the conversion is + # a content-identity transform, so we can skip the entire recursive walk. This is + # the hot path for SSE streaming, where a large discriminated union would otherwise + # be traversed on every single event. + if not _requires_conversion(annotation): + return object_ clean_type = _remove_annotations(inner_type) # Pydantic models @@ -161,7 +236,7 @@ def _convert_mapping( direction: typing.Literal["read", "write"], ) -> typing.Mapping[str, object]: converted_object: typing.Dict[str, object] = {} - annotations = typing_extensions.get_type_hints(expected_type, include_extras=True) + annotations = _get_cached_type_hints(expected_type) aliases_to_field_names = _get_alias_to_field_name(annotations) for key, value in object_.items(): if direction == "read" and key in aliases_to_field_names: @@ -217,12 +292,12 @@ def _remove_annotations(type_: typing.Any) -> typing.Any: def get_alias_to_field_mapping(type_: typing.Any) -> typing.Dict[str, str]: - annotations = typing_extensions.get_type_hints(type_, include_extras=True) + annotations = _get_cached_type_hints(type_) return _get_alias_to_field_name(annotations) def get_field_to_alias_mapping(type_: typing.Any) -> typing.Dict[str, str]: - annotations = typing_extensions.get_type_hints(type_, include_extras=True) + annotations = _get_cached_type_hints(type_) return _get_field_to_alias_name(annotations) diff --git a/src/speechify/errors/__init__.py b/src/speechify/errors/__init__.py new file mode 100644 index 0000000..a5a2b3f --- /dev/null +++ b/src/speechify/errors/__init__.py @@ -0,0 +1,65 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .bad_gateway_error import BadGatewayError + from .bad_request_error import BadRequestError + from .forbidden_error import ForbiddenError + from .internal_server_error import InternalServerError + from .not_found_error import NotFoundError + from .payment_required_error import PaymentRequiredError + from .service_unavailable_error import ServiceUnavailableError + from .too_many_requests_error import TooManyRequestsError + from .unauthorized_error import UnauthorizedError + from .unprocessable_entity_error import UnprocessableEntityError +_dynamic_imports: typing.Dict[str, str] = { + "BadGatewayError": ".bad_gateway_error", + "BadRequestError": ".bad_request_error", + "ForbiddenError": ".forbidden_error", + "InternalServerError": ".internal_server_error", + "NotFoundError": ".not_found_error", + "PaymentRequiredError": ".payment_required_error", + "ServiceUnavailableError": ".service_unavailable_error", + "TooManyRequestsError": ".too_many_requests_error", + "UnauthorizedError": ".unauthorized_error", + "UnprocessableEntityError": ".unprocessable_entity_error", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "BadGatewayError", + "BadRequestError", + "ForbiddenError", + "InternalServerError", + "NotFoundError", + "PaymentRequiredError", + "ServiceUnavailableError", + "TooManyRequestsError", + "UnauthorizedError", + "UnprocessableEntityError", +] diff --git a/src/speechify/errors/bad_gateway_error.py b/src/speechify/errors/bad_gateway_error.py new file mode 100644 index 0000000..4082381 --- /dev/null +++ b/src/speechify/errors/bad_gateway_error.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError +from ..types.error import Error + + +class BadGatewayError(ApiError): + def __init__(self, body: Error, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=502, headers=headers, body=body) diff --git a/src/speechify/errors/bad_request_error.py b/src/speechify/errors/bad_request_error.py new file mode 100644 index 0000000..ec78e26 --- /dev/null +++ b/src/speechify/errors/bad_request_error.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError + + +class BadRequestError(ApiError): + def __init__(self, body: typing.Any, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=400, headers=headers, body=body) diff --git a/src/speechify/errors/forbidden_error.py b/src/speechify/errors/forbidden_error.py new file mode 100644 index 0000000..e501c57 --- /dev/null +++ b/src/speechify/errors/forbidden_error.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError +from ..types.error import Error + + +class ForbiddenError(ApiError): + def __init__(self, body: Error, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=403, headers=headers, body=body) diff --git a/src/speechify/errors/internal_server_error.py b/src/speechify/errors/internal_server_error.py new file mode 100644 index 0000000..2c41ca5 --- /dev/null +++ b/src/speechify/errors/internal_server_error.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError +from ..types.error import Error + + +class InternalServerError(ApiError): + def __init__(self, body: Error, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=500, headers=headers, body=body) diff --git a/src/speechify/errors/not_found_error.py b/src/speechify/errors/not_found_error.py new file mode 100644 index 0000000..75f557d --- /dev/null +++ b/src/speechify/errors/not_found_error.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError + + +class NotFoundError(ApiError): + def __init__(self, body: typing.Any, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=404, headers=headers, body=body) diff --git a/src/speechify/errors/payment_required_error.py b/src/speechify/errors/payment_required_error.py new file mode 100644 index 0000000..1fbd073 --- /dev/null +++ b/src/speechify/errors/payment_required_error.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError +from ..types.error import Error + + +class PaymentRequiredError(ApiError): + def __init__(self, body: Error, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=402, headers=headers, body=body) diff --git a/src/speechify/errors/service_unavailable_error.py b/src/speechify/errors/service_unavailable_error.py new file mode 100644 index 0000000..50e2d01 --- /dev/null +++ b/src/speechify/errors/service_unavailable_error.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError +from ..types.error import Error + + +class ServiceUnavailableError(ApiError): + def __init__(self, body: Error, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=503, headers=headers, body=body) diff --git a/src/speechify/errors/too_many_requests_error.py b/src/speechify/errors/too_many_requests_error.py new file mode 100644 index 0000000..a0743ee --- /dev/null +++ b/src/speechify/errors/too_many_requests_error.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError +from ..types.error import Error + + +class TooManyRequestsError(ApiError): + def __init__(self, body: Error, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=429, headers=headers, body=body) diff --git a/src/speechify/errors/unauthorized_error.py b/src/speechify/errors/unauthorized_error.py new file mode 100644 index 0000000..7e48bb6 --- /dev/null +++ b/src/speechify/errors/unauthorized_error.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError + + +class UnauthorizedError(ApiError): + def __init__(self, body: typing.Any, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=401, headers=headers, body=body) diff --git a/src/speechify/errors/unprocessable_entity_error.py b/src/speechify/errors/unprocessable_entity_error.py new file mode 100644 index 0000000..31587f1 --- /dev/null +++ b/src/speechify/errors/unprocessable_entity_error.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError +from ..types.error import Error + + +class UnprocessableEntityError(ApiError): + def __init__(self, body: Error, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=422, headers=headers, body=body) diff --git a/src/speechify/tts/__init__.py b/src/speechify/tts/__init__.py deleted file mode 100644 index 878b91d..0000000 --- a/src/speechify/tts/__init__.py +++ /dev/null @@ -1,297 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .types import ( - AccessToken, - AccessTokenScope, - AccessTokenTokenType, - Agent, - AgentTest, - AgentTestAttachment, - AgentTestConfig, - AgentTestFolder, - AgentTestRun, - AgentTestWithLastRun, - ApiKey, - BatchRunEntry, - ClientToolConfig, - Conversation, - ConversationStats, - ConversationStatus, - ConversationTransport, - CreateConversationResponse, - CreateVoiceLanguage, - CreateVoiceModel, - CreateVoiceModelName, - CreatedVoice, - CreatedVoiceGender, - CreatedVoiceType, - DataCollectionField, - DataCollectionFieldType, - DeleteMemoriesByCallerResponse, - DynamicVariable, - DynamicVariableType, - Evaluation, - EvaluationConfig, - EvaluationCriterion, - EvaluationKind, - EvaluationStatus, - GetSpeechOptionsRequest, - GetSpeechResponse, - GetSpeechResponseAudioFormat, - GetStreamOptionsRequest, - GetVoice, - GetVoiceGender, - GetVoiceLanguage, - GetVoiceType, - GetVoicesModel, - GetVoicesModelName, - Invite, - InvitePreview, - InvitesListResponse, - KnowledgeBase, - KnowledgeBaseChunk, - KnowledgeBaseDocument, - KnowledgeBaseDocumentStatus, - KnowledgeBaseSearchHit, - ListAgentTestAttachmentsResponse, - ListAgentTestFoldersResponse, - ListAgentTestRunsResponse, - ListAgentTestsResponse, - ListAgentsResponse, - ListConversationsResponse, - ListDynamicVariablesResponse, - ListEvaluationsResponse, - ListKnowledgeBaseChunksResponse, - ListKnowledgeBaseDocumentsResponse, - ListKnowledgeBasesResponse, - ListMemoriesResponse, - ListMessagesResponse, - ListTestsResponse, - ListToolsResponse, - Member, - MemberRole, - MembersListResponse, - Memory, - Message, - MessageRole, - MockingStrategy, - NestedChunk, - NoMatchBehavior, - OAuthError, - OAuthErrorError, - ParameterCheck, - ParameterCheckMode, - ParameterCheckResult, - RunAgentTestsResponse, - RunBatchResponse, - ScenarioConfig, - ScenarioResult, - SearchKnowledgeBasesResponse, - SimulationConfig, - SimulationMessage, - SimulationMessageRole, - SimulationResult, - SimulationToolCall, - SpeechMarks, - SystemToolConfig, - SystemToolConfigBuiltin, - SystemVariableDoc, - Tenant, - TenantDataRegion, - TenantPlan, - TenantsListResponse, - TestRunResult, - TestRunStatus, - TestStats, - TestStatsBucket, - TestType, - Tool, - ToolCallConfig, - ToolCallResult, - ToolConfig, - ToolKind, - ToolMock, - ToolMockConfig, - ToolParam, - ToolParamType, - WebhookToolConfig, - WebhookToolConfigMethod, -) -from .errors import ( - BadRequestError, - ConflictError, - ContentTooLargeError, - ForbiddenError, - InternalServerError, - NotFoundError, - PaymentRequiredError, - UnauthorizedError, -) -from . import ( - agents, - audio, - auth, - conversations, - knowledge_bases, - memories, - outbound_calls, - phone_numbers, - sip_trunks, - tools, - voices, - workspaces, -) -from .agents import CreateAgentTestRequestConfig, UpdateAgentTestRequestConfig -from .audio import GetSpeechRequestAudioFormat, GetSpeechRequestModel, GetStreamRequestModel, StreamAudioRequestAccept -from .auth import CreateAccessTokenRequestGrantType, CreateAccessTokenRequestScope -from .tools import CreateToolRequestConfig, UpdateToolRequestConfig -from .voices import CreateVoicesRequestGender - -__all__ = [ - "AccessToken", - "AccessTokenScope", - "AccessTokenTokenType", - "Agent", - "AgentTest", - "AgentTestAttachment", - "AgentTestConfig", - "AgentTestFolder", - "AgentTestRun", - "AgentTestWithLastRun", - "ApiKey", - "BadRequestError", - "BatchRunEntry", - "ClientToolConfig", - "ConflictError", - "ContentTooLargeError", - "Conversation", - "ConversationStats", - "ConversationStatus", - "ConversationTransport", - "CreateAccessTokenRequestGrantType", - "CreateAccessTokenRequestScope", - "CreateAgentTestRequestConfig", - "CreateConversationResponse", - "CreateToolRequestConfig", - "CreateVoiceLanguage", - "CreateVoiceModel", - "CreateVoiceModelName", - "CreateVoicesRequestGender", - "CreatedVoice", - "CreatedVoiceGender", - "CreatedVoiceType", - "DataCollectionField", - "DataCollectionFieldType", - "DeleteMemoriesByCallerResponse", - "DynamicVariable", - "DynamicVariableType", - "Evaluation", - "EvaluationConfig", - "EvaluationCriterion", - "EvaluationKind", - "EvaluationStatus", - "ForbiddenError", - "GetSpeechOptionsRequest", - "GetSpeechRequestAudioFormat", - "GetSpeechRequestModel", - "GetSpeechResponse", - "GetSpeechResponseAudioFormat", - "GetStreamOptionsRequest", - "GetStreamRequestModel", - "GetVoice", - "GetVoiceGender", - "GetVoiceLanguage", - "GetVoiceType", - "GetVoicesModel", - "GetVoicesModelName", - "InternalServerError", - "Invite", - "InvitePreview", - "InvitesListResponse", - "KnowledgeBase", - "KnowledgeBaseChunk", - "KnowledgeBaseDocument", - "KnowledgeBaseDocumentStatus", - "KnowledgeBaseSearchHit", - "ListAgentTestAttachmentsResponse", - "ListAgentTestFoldersResponse", - "ListAgentTestRunsResponse", - "ListAgentTestsResponse", - "ListAgentsResponse", - "ListConversationsResponse", - "ListDynamicVariablesResponse", - "ListEvaluationsResponse", - "ListKnowledgeBaseChunksResponse", - "ListKnowledgeBaseDocumentsResponse", - "ListKnowledgeBasesResponse", - "ListMemoriesResponse", - "ListMessagesResponse", - "ListTestsResponse", - "ListToolsResponse", - "Member", - "MemberRole", - "MembersListResponse", - "Memory", - "Message", - "MessageRole", - "MockingStrategy", - "NestedChunk", - "NoMatchBehavior", - "NotFoundError", - "OAuthError", - "OAuthErrorError", - "ParameterCheck", - "ParameterCheckMode", - "ParameterCheckResult", - "PaymentRequiredError", - "RunAgentTestsResponse", - "RunBatchResponse", - "ScenarioConfig", - "ScenarioResult", - "SearchKnowledgeBasesResponse", - "SimulationConfig", - "SimulationMessage", - "SimulationMessageRole", - "SimulationResult", - "SimulationToolCall", - "SpeechMarks", - "StreamAudioRequestAccept", - "SystemToolConfig", - "SystemToolConfigBuiltin", - "SystemVariableDoc", - "Tenant", - "TenantDataRegion", - "TenantPlan", - "TenantsListResponse", - "TestRunResult", - "TestRunStatus", - "TestStats", - "TestStatsBucket", - "TestType", - "Tool", - "ToolCallConfig", - "ToolCallResult", - "ToolConfig", - "ToolKind", - "ToolMock", - "ToolMockConfig", - "ToolParam", - "ToolParamType", - "UnauthorizedError", - "UpdateAgentTestRequestConfig", - "UpdateToolRequestConfig", - "WebhookToolConfig", - "WebhookToolConfigMethod", - "agents", - "audio", - "auth", - "conversations", - "knowledge_bases", - "memories", - "outbound_calls", - "phone_numbers", - "sip_trunks", - "tools", - "voices", - "workspaces", -] diff --git a/src/speechify/tts/agents/__init__.py b/src/speechify/tts/agents/__init__.py deleted file mode 100644 index 1e56e81..0000000 --- a/src/speechify/tts/agents/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .types import CreateAgentTestRequestConfig, UpdateAgentTestRequestConfig - -__all__ = ["CreateAgentTestRequestConfig", "UpdateAgentTestRequestConfig"] diff --git a/src/speechify/tts/agents/client.py b/src/speechify/tts/agents/client.py deleted file mode 100644 index 17433d1..0000000 --- a/src/speechify/tts/agents/client.py +++ /dev/null @@ -1,6604 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from ...core.client_wrapper import SyncClientWrapper -from ...core.request_options import RequestOptions -from ..types.list_agents_response import ListAgentsResponse -from ...core.pydantic_utilities import parse_obj_as -from ..errors.unauthorized_error import UnauthorizedError -from json.decoder import JSONDecodeError -from ...core.api_error import ApiError -from ..types.agent import Agent -from ..errors.bad_request_error import BadRequestError -from ...core.jsonable_encoder import jsonable_encoder -from ..errors.not_found_error import NotFoundError -from ..types.list_tools_response import ListToolsResponse -from ..types.evaluation_config import EvaluationConfig -from ..types.evaluation_criterion import EvaluationCriterion -from ..types.data_collection_field import DataCollectionField -from ...core.serialization import convert_and_respect_annotation_metadata -from ..types.list_dynamic_variables_response import ListDynamicVariablesResponse -from ..types.dynamic_variable import DynamicVariable -from ..types.create_conversation_response import CreateConversationResponse -from ..errors.forbidden_error import ForbiddenError -from ..types.list_knowledge_bases_response import ListKnowledgeBasesResponse -from ..types.list_memories_response import ListMemoriesResponse -from ..types.delete_memories_by_caller_response import DeleteMemoriesByCallerResponse -from ..types.list_agent_tests_response import ListAgentTestsResponse -from ..types.test_type import TestType -from .types.create_agent_test_request_config import CreateAgentTestRequestConfig -from ..types.tool_mock_config import ToolMockConfig -from ..types.agent_test import AgentTest -from ..types.run_agent_tests_response import RunAgentTestsResponse -from .types.update_agent_test_request_config import UpdateAgentTestRequestConfig -from ..types.list_agent_test_runs_response import ListAgentTestRunsResponse -from ..types.agent_test_run import AgentTestRun -from ..types.list_tests_response import ListTestsResponse -from ..types.test_stats import TestStats -from ..types.batch_run_entry import BatchRunEntry -from ..types.run_batch_response import RunBatchResponse -from ..types.list_agent_test_attachments_response import ListAgentTestAttachmentsResponse -from ..types.list_agent_test_folders_response import ListAgentTestFoldersResponse -from ..types.agent_test_folder import AgentTestFolder -from ...core.client_wrapper import AsyncClientWrapper - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class AgentsClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> ListAgentsResponse: - """ - List voice agents owned by the caller. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListAgentsResponse - A list of voice agents. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.list() - """ - _response = self._client_wrapper.httpx_client.request( - "v1/agents", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListAgentsResponse, - parse_obj_as( - type_=ListAgentsResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def create( - self, - *, - name: str, - voice_id: str, - slug: typing.Optional[str] = OMIT, - prompt: typing.Optional[str] = OMIT, - first_message: typing.Optional[str] = OMIT, - language: typing.Optional[str] = OMIT, - llm_model: typing.Optional[str] = OMIT, - temperature: typing.Optional[float] = OMIT, - config: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = OMIT, - is_public: typing.Optional[bool] = OMIT, - allowed_origins: typing.Optional[typing.Sequence[str]] = OMIT, - hostname_allowlist: typing.Optional[typing.Sequence[str]] = OMIT, - memory_enabled: typing.Optional[bool] = OMIT, - memory_retention_days: typing.Optional[int] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> Agent: - """ - Create a voice agent. - - Parameters - ---------- - name : str - - voice_id : str - Voice slug from the VMS catalog (see GET /v1/voices). Required — the server rejects writes with an unknown or empty slug. - - slug : typing.Optional[str] - Optional. Server derives slug from name with a random suffix when omitted; if you supply your own, a collision returns 400 'slug already taken'. - - prompt : typing.Optional[str] - - first_message : typing.Optional[str] - Spoken verbatim at session start — no LLM round trip. - - language : typing.Optional[str] - - llm_model : typing.Optional[str] - Optional chat model slug. Leave empty to use the Speechify default. - - temperature : typing.Optional[float] - - config : typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] - - is_public : typing.Optional[bool] - - allowed_origins : typing.Optional[typing.Sequence[str]] - - hostname_allowlist : typing.Optional[typing.Sequence[str]] - Optional per-agent hostname allowlist (see Agent schema). - - memory_enabled : typing.Optional[bool] - - memory_retention_days : typing.Optional[int] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Agent - The created agent. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.create( - name="name", - voice_id="voice_id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - "v1/agents", - method="POST", - json={ - "name": name, - "slug": slug, - "prompt": prompt, - "first_message": first_message, - "language": language, - "llm_model": llm_model, - "voice_id": voice_id, - "temperature": temperature, - "config": config, - "is_public": is_public, - "allowed_origins": allowed_origins, - "hostname_allowlist": hostname_allowlist, - "memory_enabled": memory_enabled, - "memory_retention_days": memory_retention_days, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - Agent, - parse_obj_as( - type_=Agent, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def get(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> Agent: - """ - Retrieve a voice agent by ID. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Agent - The requested agent. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.get( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - Agent, - parse_obj_as( - type_=Agent, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def delete(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: - """ - Delete a voice agent. Conversations and attached tools remain. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.delete( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def update( - self, - id: str, - *, - name: typing.Optional[str] = OMIT, - prompt: typing.Optional[str] = OMIT, - first_message: typing.Optional[str] = OMIT, - language: typing.Optional[str] = OMIT, - llm_model: typing.Optional[str] = OMIT, - voice_id: typing.Optional[str] = OMIT, - temperature: typing.Optional[float] = OMIT, - config: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = OMIT, - is_public: typing.Optional[bool] = OMIT, - allowed_origins: typing.Optional[typing.Sequence[str]] = OMIT, - hostname_allowlist: typing.Optional[typing.Sequence[str]] = OMIT, - memory_enabled: typing.Optional[bool] = OMIT, - memory_retention_days: typing.Optional[int] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> Agent: - """ - Update a voice agent. Only fields present on the request body are changed. - - Parameters - ---------- - id : str - - name : typing.Optional[str] - - prompt : typing.Optional[str] - - first_message : typing.Optional[str] - - language : typing.Optional[str] - - llm_model : typing.Optional[str] - - voice_id : typing.Optional[str] - - temperature : typing.Optional[float] - - config : typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] - - is_public : typing.Optional[bool] - - allowed_origins : typing.Optional[typing.Sequence[str]] - - hostname_allowlist : typing.Optional[typing.Sequence[str]] - When supplied, replaces the stored list. Pass an empty - array to clear enforcement (public agent is open again). - Omit the field to leave the existing value unchanged. - - memory_enabled : typing.Optional[bool] - - memory_retention_days : typing.Optional[int] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Agent - The updated agent. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.update( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}", - method="PATCH", - json={ - "name": name, - "prompt": prompt, - "first_message": first_message, - "language": language, - "llm_model": llm_model, - "voice_id": voice_id, - "temperature": temperature, - "config": config, - "is_public": is_public, - "allowed_origins": allowed_origins, - "hostname_allowlist": hostname_allowlist, - "memory_enabled": memory_enabled, - "memory_retention_days": memory_retention_days, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - Agent, - parse_obj_as( - type_=Agent, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def list_tools(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> ListToolsResponse: - """ - List tools currently attached to the agent. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListToolsResponse - Attached tools for the agent. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.list_tools( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}/tools", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListToolsResponse, - parse_obj_as( - type_=ListToolsResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def attach_tool(self, id: str, tool_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: - """ - Attach an existing tool to the agent so the LLM can call it. - - Parameters - ---------- - id : str - - tool_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.attach_tool( - id="id", - tool_id="toolId", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}/tools/{jsonable_encoder(tool_id)}", - method="POST", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def detach_tool(self, id: str, tool_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: - """ - Detach a tool from the agent. - - Parameters - ---------- - id : str - - tool_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.detach_tool( - id="id", - tool_id="toolId", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}/tools/{jsonable_encoder(tool_id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def get_evaluation_config( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> EvaluationConfig: - """ - Retrieve the agent's post-call evaluation criteria + data-collection config. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - EvaluationConfig - The evaluation config for the agent. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.get_evaluation_config( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}/evaluation-config", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - EvaluationConfig, - parse_obj_as( - type_=EvaluationConfig, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def update_evaluation_config( - self, - id: str, - *, - criteria: typing.Sequence[EvaluationCriterion], - data_collection: typing.Sequence[DataCollectionField], - request_options: typing.Optional[RequestOptions] = None, - ) -> EvaluationConfig: - """ - Replace the agent's evaluation criteria + data-collection fields. - - Parameters - ---------- - id : str - - criteria : typing.Sequence[EvaluationCriterion] - - data_collection : typing.Sequence[DataCollectionField] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - EvaluationConfig - The updated evaluation config. - - Examples - -------- - from speechify import Speechify - from speechify.tts import DataCollectionField, EvaluationCriterion - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.update_evaluation_config( - id="id", - criteria=[ - EvaluationCriterion( - id="id", - name="name", - description="description", - ) - ], - data_collection=[ - DataCollectionField( - key="key", - description="description", - type="string", - ) - ], - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}/evaluation-config", - method="PATCH", - json={ - "criteria": convert_and_respect_annotation_metadata( - object_=criteria, annotation=typing.Sequence[EvaluationCriterion], direction="write" - ), - "data_collection": convert_and_respect_annotation_metadata( - object_=data_collection, annotation=typing.Sequence[DataCollectionField], direction="write" - ), - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - EvaluationConfig, - parse_obj_as( - type_=EvaluationConfig, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def get_dynamic_variables( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> ListDynamicVariablesResponse: - """ - Retrieve the agent's customer-scope dynamic variables and the read-only - catalogue of reserved `system__*` keys. The system variables list is - provided so editor UIs can render the reference list without maintaining - a client-side copy of the catalogue. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListDynamicVariablesResponse - The agent's variable catalogue. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.get_dynamic_variables( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}/variables", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListDynamicVariablesResponse, - parse_obj_as( - type_=ListDynamicVariablesResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def update_dynamic_variables( - self, - id: str, - *, - variables: typing.Sequence[DynamicVariable], - request_options: typing.Optional[RequestOptions] = None, - ) -> ListDynamicVariablesResponse: - """ - Replace the agent's customer-scope dynamic variable definitions. - The supplied list overwrites the stored list wholesale (same - semantics as `updateEvaluationConfig`). Pass an empty array to - clear all variables. Up to 20 variables per agent. Keys must - match `[a-zA-Z0-9_]+` and must not start with the reserved - `system__` prefix. - - Parameters - ---------- - id : str - - variables : typing.Sequence[DynamicVariable] - The new variable list. Replaces the existing list entirely. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListDynamicVariablesResponse - The updated variable catalogue. - - Examples - -------- - from speechify import Speechify - from speechify.tts import DynamicVariable - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.update_dynamic_variables( - id="id", - variables=[ - DynamicVariable( - key="product_name", - type="string", - default="Speechify", - description="Product the agent is supporting.", - ), - DynamicVariable( - key="support_tier", - type="number", - default=1, - ), - DynamicVariable( - key="account_metadata", - type="json", - description="Arbitrary account context injected into tool bodies.", - ), - ], - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}/variables", - method="PATCH", - json={ - "variables": convert_and_respect_annotation_metadata( - object_=variables, annotation=typing.Sequence[DynamicVariable], direction="write" - ), - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListDynamicVariablesResponse, - parse_obj_as( - type_=ListDynamicVariablesResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def create_conversation( - self, - id: str, - *, - transport: typing.Optional[str] = OMIT, - dynamic_variables: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> CreateConversationResponse: - """ - Start a new voice conversation with the agent. Returns a realtime - voice session + short-lived client token so the caller can - connect the audio pipeline directly. The agent is dispatched - server-side; no additional client action required. - - Pass `dynamic_variables` to supply per-session values that override - the agent's stored variable defaults for this one conversation. - Keys in the `system__` namespace are rejected at this boundary. - - Parameters - ---------- - id : str - - transport : typing.Optional[str] - Transport hint. Omit to use the agent's default. - - dynamic_variables : typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] - Per-session variable overrides that merge on top of the agent's - stored variable defaults for this one conversation. Keys in the - reserved `system__` namespace are rejected. Values must match the - declared type of the corresponding variable definition on the agent. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - CreateConversationResponse - The created conversation with its realtime session token. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.create_conversation( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}/conversations", - method="POST", - json={ - "transport": transport, - "dynamic_variables": dynamic_variables, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - CreateConversationResponse, - parse_obj_as( - type_=CreateConversationResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def create_session( - self, - id: str, - *, - user_identity: typing.Optional[str] = OMIT, - dynamic_variables: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> CreateConversationResponse: - """ - Mint a realtime voice session for the given agent. Widget-friendly - counterpart to `createConversation` — same response shape, dual - authentication: - - * **Authenticated (Bearer)**: works for any agent the caller - owns. Typical server-to-server flow where the embedding - site's backend mints a token and hands it to the browser so - the API key never reaches the client. - * **Unauthenticated**: works only when `agent.is_public = true` - AND the request's `Origin` header matches `agent.allowed_origins` - (or that list is empty). When `agent.hostname_allowlist` is - non-empty, the `Origin` hostname must additionally be a - member of that list. Used directly by the - `` web component. - - Responds with the same `CreateConversationResponse` as - `createConversation`. - - Parameters - ---------- - id : str - - user_identity : typing.Optional[str] - Opaque identifier for the end-user (e.g. your app's user ID). Stamped onto the conversation. Optional - defaults to an anonymous per-session ID. - - dynamic_variables : typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] - Per-session variable overrides that merge on top of the agent's - stored variable defaults for this one session. Keys in the - reserved `system__` namespace are rejected at this boundary. - Values must match the declared type of the corresponding variable - definition on the agent (a `string` type expects a JSON string, - `number` expects a JSON number, etc.). - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - CreateConversationResponse - The created session with its realtime token + URL. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.create_session( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}/sessions", - method="POST", - json={ - "user_identity": user_identity, - "dynamic_variables": dynamic_variables, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - CreateConversationResponse, - parse_obj_as( - type_=CreateConversationResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 403: - raise ForbiddenError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def list_agent_knowledge_bases( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> ListKnowledgeBasesResponse: - """ - List knowledge bases attached to an agent. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListKnowledgeBasesResponse - The knowledge bases attached to the agent. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.list_agent_knowledge_bases( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}/knowledge-bases", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListKnowledgeBasesResponse, - parse_obj_as( - type_=ListKnowledgeBasesResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def attach_knowledge_base( - self, id: str, kb_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> None: - """ - Attach a knowledge base to an agent. The `search_knowledge` tool - is auto-registered on the next conversation and can only query the - attached knowledge bases. - - Parameters - ---------- - id : str - - kb_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.attach_knowledge_base( - id="id", - kb_id="kbId", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}/knowledge-bases/{jsonable_encoder(kb_id)}", - method="POST", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def detach_knowledge_base( - self, id: str, kb_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> None: - """ - Detach a knowledge base from an agent. - - Parameters - ---------- - id : str - - kb_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.detach_knowledge_base( - id="id", - kb_id="kbId", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}/knowledge-bases/{jsonable_encoder(kb_id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def list_memories( - self, - id: str, - *, - limit: typing.Optional[int] = None, - offset: typing.Optional[int] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> ListMemoriesResponse: - """ - List per-caller memories extracted for an agent. Memories are - written post-call by the built-in extractor when `memory_enabled` - is true on the agent; the list is sorted newest-first. - - Parameters - ---------- - id : str - - limit : typing.Optional[int] - Maximum rows to return. Defaults to 100, capped at 200. - - offset : typing.Optional[int] - Number of rows to skip. Combine with `limit` to page through older memories. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListMemoriesResponse - Memories for the agent. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.list_memories( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}/memories", - method="GET", - params={ - "limit": limit, - "offset": offset, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListMemoriesResponse, - parse_obj_as( - type_=ListMemoriesResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def delete_memories_by_caller( - self, id: str, *, agent_id: str, caller_identity: str, request_options: typing.Optional[RequestOptions] = None - ) -> DeleteMemoriesByCallerResponse: - """ - Delete every memory ever extracted for a specific caller on - this agent. Privacy / GDPR surface. Returns the count of rows - soft-deleted; rows become permanently unreachable immediately - and are hard-deleted by the retention job after the tenant's - configured retention window. - - Parameters - ---------- - id : str - - agent_id : str - - caller_identity : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - DeleteMemoriesByCallerResponse - Deletion summary. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.delete_memories_by_caller( - id="id", - agent_id="agent_id", - caller_identity="caller_identity", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}/memories/delete-by-caller", - method="POST", - json={ - "agent_id": agent_id, - "caller_identity": caller_identity, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - DeleteMemoriesByCallerResponse, - parse_obj_as( - type_=DeleteMemoriesByCallerResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def list_tests(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> ListAgentTestsResponse: - """ - List all tests configured for the agent. Each entry includes the - most recent run so the console can render pass/fail badges without - an extra round-trip. - - Parameters - ---------- - id : str - Agent ID. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListAgentTestsResponse - Tests for the agent with last-run summaries. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.list_tests( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}/tests", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListAgentTestsResponse, - parse_obj_as( - type_=ListAgentTestsResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def create_test( - self, - id: str, - *, - name: str, - type: TestType, - config: CreateAgentTestRequestConfig, - description: typing.Optional[str] = OMIT, - tool_mock_config: typing.Optional[ToolMockConfig] = OMIT, - variables: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = OMIT, - folder_id: typing.Optional[str] = OMIT, - attached_agent_ids: typing.Optional[typing.Sequence[str]] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> AgentTest: - """ - Create a new test for the agent. - - Parameters - ---------- - id : str - Agent ID. - - name : str - Short human-readable label for the test. - - type : TestType - - config : CreateAgentTestRequestConfig - Type-specific configuration. Must match the shape for the given `type`. - - description : typing.Optional[str] - Optional longer description of what this test verifies. - - tool_mock_config : typing.Optional[ToolMockConfig] - Optional tool-mocking config applied during every run of this test. - - variables : typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] - Per-test variable values substituted into string fields of the - config at run-start. Keys use the same rules as agent-level - `DynamicVariable` keys. - - folder_id : typing.Optional[str] - Folder to place the test in. Omit for root. - - attached_agent_ids : typing.Optional[typing.Sequence[str]] - Optional list of additional agents this test should also run - against. The owner agent (path param) is always attached - implicitly. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AgentTest - The created test. - - Examples - -------- - from speechify import Speechify - from speechify.tts import ScenarioConfig - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.create_test( - id="id", - name="Greet the caller by name", - description="Agent should greet the caller using their name when provided.", - type="scenario", - config=ScenarioConfig( - context="The caller says: Hi, I'm Alice.", - success_criteria="The agent greets Alice by name.", - success_examples=["Hi Alice! How can I help you today?"], - failure_examples=["Hello! How can I help you?"], - ), - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}/tests", - method="POST", - json={ - "name": name, - "description": description, - "type": type, - "config": convert_and_respect_annotation_metadata( - object_=config, annotation=CreateAgentTestRequestConfig, direction="write" - ), - "tool_mock_config": convert_and_respect_annotation_metadata( - object_=tool_mock_config, annotation=ToolMockConfig, direction="write" - ), - "variables": variables, - "folder_id": folder_id, - "attached_agent_ids": attached_agent_ids, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - AgentTest, - parse_obj_as( - type_=AgentTest, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def run_all_tests( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> RunAgentTestsResponse: - """ - Enqueue runs for every test on the agent concurrently. Up to 50 - tests are dispatched in one call. Each returned run starts in - `queued` status; poll `GET /v1/test-runs/{id}` for the terminal - result. - - Parameters - ---------- - id : str - Agent ID. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - RunAgentTestsResponse - Queued runs for all tests on the agent. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.run_all_tests( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}/tests/runs", - method="POST", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - RunAgentTestsResponse, - parse_obj_as( - type_=RunAgentTestsResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def get_test(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> AgentTest: - """ - Retrieve a test by ID. - - Parameters - ---------- - id : str - Test ID. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AgentTest - The requested test. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.get_test( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/tests/{jsonable_encoder(id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - AgentTest, - parse_obj_as( - type_=AgentTest, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def delete_test(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: - """ - Delete a test and all its run history. - - Parameters - ---------- - id : str - Test ID. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.delete_test( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/tests/{jsonable_encoder(id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def update_test( - self, - id: str, - *, - name: typing.Optional[str] = OMIT, - description: typing.Optional[str] = OMIT, - config: typing.Optional[UpdateAgentTestRequestConfig] = OMIT, - tool_mock_config: typing.Optional[ToolMockConfig] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> AgentTest: - """ - Update a test. Only fields present on the request body are changed. - - Parameters - ---------- - id : str - Test ID. - - name : typing.Optional[str] - - description : typing.Optional[str] - - config : typing.Optional[UpdateAgentTestRequestConfig] - Replaces the test config when present. - - tool_mock_config : typing.Optional[ToolMockConfig] - Replaces the tool-mock config when present. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AgentTest - The updated test. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.update_test( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/tests/{jsonable_encoder(id)}", - method="PATCH", - json={ - "name": name, - "description": description, - "config": convert_and_respect_annotation_metadata( - object_=config, annotation=UpdateAgentTestRequestConfig, direction="write" - ), - "tool_mock_config": convert_and_respect_annotation_metadata( - object_=tool_mock_config, annotation=ToolMockConfig, direction="write" - ), - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - AgentTest, - parse_obj_as( - type_=AgentTest, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def list_test_runs( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> ListAgentTestRunsResponse: - """ - List the run history for a test, newest first. - - Parameters - ---------- - id : str - Test ID. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListAgentTestRunsResponse - Run history for the test. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.list_test_runs( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/tests/{jsonable_encoder(id)}/runs", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListAgentTestRunsResponse, - parse_obj_as( - type_=ListAgentTestRunsResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def run_test(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> AgentTestRun: - """ - Enqueue a single run of the test. The returned run starts in - `queued` status. Poll `GET /v1/test-runs/{id}` until the status - reaches a terminal state (`passed`, `failed`, or `error`). - - Parameters - ---------- - id : str - Test ID. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AgentTestRun - The queued run. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.run_test( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/tests/{jsonable_encoder(id)}/runs", - method="POST", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - AgentTestRun, - parse_obj_as( - type_=AgentTestRun, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def get_test_run(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> AgentTestRun: - """ - Retrieve a single test run by ID. Poll this endpoint until - `status` reaches a terminal state (`passed`, `failed`, or `error`). - The `result` field is populated on terminal states. - - Parameters - ---------- - id : str - Test run ID. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AgentTestRun - The test run. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.get_test_run( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/test-runs/{jsonable_encoder(id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - AgentTestRun, - parse_obj_as( - type_=AgentTestRun, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def list_all_tests( - self, - *, - agent_id: typing.Optional[str] = None, - type: typing.Optional[str] = None, - status: typing.Optional[str] = None, - folder_id: typing.Optional[str] = None, - updated_after: typing.Optional[str] = None, - q: typing.Optional[str] = None, - limit: typing.Optional[int] = None, - cursor: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> ListTestsResponse: - """ - Workspace-wide list of tests across every agent the caller owns. - Supports filters (agent, type, last-run status, folder), full-text - search on name/description, and cursor pagination. Each row carries - its newest run and attached agent IDs so the list renders without - N+1 round-trips. - - Parameters - ---------- - agent_id : typing.Optional[str] - Comma-separated agent IDs to filter on. - - type : typing.Optional[str] - Comma-separated test types (scenario|tool|simulation). - - status : typing.Optional[str] - Comma-separated last-run statuses. - - folder_id : typing.Optional[str] - Folder ID to filter on, or "root" for unfiled tests. - - updated_after : typing.Optional[str] - Only return tests updated after this RFC3339 timestamp. - - q : typing.Optional[str] - Substring match on name or description. - - limit : typing.Optional[int] - Max tests per page (default 50, max 200). - - cursor : typing.Optional[str] - Opaque pagination cursor from a previous response. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListTestsResponse - Paginated list. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.list_all_tests() - """ - _response = self._client_wrapper.httpx_client.request( - "v1/tests", - method="GET", - params={ - "agent_id": agent_id, - "type": type, - "status": status, - "folder_id": folder_id, - "updated_after": updated_after, - "q": q, - "limit": limit, - "cursor": cursor, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListTestsResponse, - parse_obj_as( - type_=ListTestsResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def get_test_stats( - self, *, window_days: typing.Optional[int] = None, request_options: typing.Optional[RequestOptions] = None - ) -> TestStats: - """ - Aggregate pass-rate metrics over the last N days. Returns dense - daily buckets (one entry per day, zero-filled) plus totals and a - per-type breakdown. Powers the header chart on the global tests - page. Default window is 30 days, max 90. - - Parameters - ---------- - window_days : typing.Optional[int] - Trailing window in days (default 30, max 90). - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - TestStats - Stats payload. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.get_test_stats() - """ - _response = self._client_wrapper.httpx_client.request( - "v1/tests/stats", - method="GET", - params={ - "window_days": window_days, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - TestStats, - parse_obj_as( - type_=TestStats, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def run_tests_batch( - self, *, entries: typing.Sequence[BatchRunEntry], request_options: typing.Optional[RequestOptions] = None - ) -> RunBatchResponse: - """ - Queue runs for every (test, agent) pair in the body. Entries - without an `agent_id` fan out to every agent the test is - attached to. Total expanded runs are capped at 100 per call. - Each entry in the response is a queued run; poll - `GET /v1/test-runs/{id}` for each. - - Parameters - ---------- - entries : typing.Sequence[BatchRunEntry] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - RunBatchResponse - Runs queued. - - Examples - -------- - from speechify import Speechify - from speechify.tts import BatchRunEntry - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.run_tests_batch( - entries=[ - BatchRunEntry( - test_id="test_id", - ) - ], - ) - """ - _response = self._client_wrapper.httpx_client.request( - "v1/tests/runs:batch", - method="POST", - json={ - "entries": convert_and_respect_annotation_metadata( - object_=entries, annotation=typing.Sequence[BatchRunEntry], direction="write" - ), - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - RunBatchResponse, - parse_obj_as( - type_=RunBatchResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def list_test_attachments( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> ListAgentTestAttachmentsResponse: - """ - List every agent a test is attached to. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListAgentTestAttachmentsResponse - Attachment list. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.list_test_attachments( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/tests/{jsonable_encoder(id)}/attachments", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListAgentTestAttachmentsResponse, - parse_obj_as( - type_=ListAgentTestAttachmentsResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def attach_test(self, id: str, agent_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: - """ - Attach a test to an additional agent. After this call, the test - will also run as part of that agent's regression suite (and - against its prompt + tool config when invoked with - `agent_id = {agentId}`). Idempotent. - - Parameters - ---------- - id : str - - agent_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.attach_test( - id="id", - agent_id="agentId", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/tests/{jsonable_encoder(id)}/attachments/{jsonable_encoder(agent_id)}", - method="POST", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def detach_test(self, id: str, agent_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: - """ - Detach a test from an agent. The owner agent (the agent the test - was authored against) cannot be detached; delete the test - instead. - - Parameters - ---------- - id : str - - agent_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.detach_test( - id="id", - agent_id="agentId", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/tests/{jsonable_encoder(id)}/attachments/{jsonable_encoder(agent_id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def move_test( - self, - id: str, - *, - folder_id: typing.Optional[str] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> None: - """ - Move a test into a folder. Pass `folder_id: null` for root. - - Parameters - ---------- - id : str - - folder_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.move_test( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/tests/{jsonable_encoder(id)}/move", - method="POST", - json={ - "folder_id": folder_id, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def list_test_folders( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> ListAgentTestFoldersResponse: - """ - List every test folder the caller owns. Flat list; build the tree client-side. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListAgentTestFoldersResponse - Folder list. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.list_test_folders() - """ - _response = self._client_wrapper.httpx_client.request( - "v1/test-folders", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListAgentTestFoldersResponse, - parse_obj_as( - type_=ListAgentTestFoldersResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def create_test_folder( - self, - *, - name: str, - parent_folder_id: typing.Optional[str] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> AgentTestFolder: - """ - Create a test folder. Max depth is 3. - - Parameters - ---------- - name : str - - parent_folder_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AgentTestFolder - Created folder. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.create_test_folder( - name="name", - ) - """ - _response = self._client_wrapper.httpx_client.request( - "v1/test-folders", - method="POST", - json={ - "name": name, - "parent_folder_id": parent_folder_id, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - AgentTestFolder, - parse_obj_as( - type_=AgentTestFolder, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def delete_test_folder(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: - """ - Soft-delete a folder. Child tests drop back to root. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.delete_test_folder( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/test-folders/{jsonable_encoder(id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def update_test_folder( - self, - id: str, - *, - name: typing.Optional[str] = OMIT, - parent_folder_id: typing.Optional[str] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> AgentTestFolder: - """ - Rename or reparent a test folder. Cycles are rejected. - - Parameters - ---------- - id : str - - name : typing.Optional[str] - - parent_folder_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AgentTestFolder - Updated folder. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.agents.update_test_folder( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/test-folders/{jsonable_encoder(id)}", - method="PATCH", - json={ - "name": name, - "parent_folder_id": parent_folder_id, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - AgentTestFolder, - parse_obj_as( - type_=AgentTestFolder, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - -class AsyncAgentsClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> ListAgentsResponse: - """ - List voice agents owned by the caller. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListAgentsResponse - A list of voice agents. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.list() - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/agents", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListAgentsResponse, - parse_obj_as( - type_=ListAgentsResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def create( - self, - *, - name: str, - voice_id: str, - slug: typing.Optional[str] = OMIT, - prompt: typing.Optional[str] = OMIT, - first_message: typing.Optional[str] = OMIT, - language: typing.Optional[str] = OMIT, - llm_model: typing.Optional[str] = OMIT, - temperature: typing.Optional[float] = OMIT, - config: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = OMIT, - is_public: typing.Optional[bool] = OMIT, - allowed_origins: typing.Optional[typing.Sequence[str]] = OMIT, - hostname_allowlist: typing.Optional[typing.Sequence[str]] = OMIT, - memory_enabled: typing.Optional[bool] = OMIT, - memory_retention_days: typing.Optional[int] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> Agent: - """ - Create a voice agent. - - Parameters - ---------- - name : str - - voice_id : str - Voice slug from the VMS catalog (see GET /v1/voices). Required — the server rejects writes with an unknown or empty slug. - - slug : typing.Optional[str] - Optional. Server derives slug from name with a random suffix when omitted; if you supply your own, a collision returns 400 'slug already taken'. - - prompt : typing.Optional[str] - - first_message : typing.Optional[str] - Spoken verbatim at session start — no LLM round trip. - - language : typing.Optional[str] - - llm_model : typing.Optional[str] - Optional chat model slug. Leave empty to use the Speechify default. - - temperature : typing.Optional[float] - - config : typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] - - is_public : typing.Optional[bool] - - allowed_origins : typing.Optional[typing.Sequence[str]] - - hostname_allowlist : typing.Optional[typing.Sequence[str]] - Optional per-agent hostname allowlist (see Agent schema). - - memory_enabled : typing.Optional[bool] - - memory_retention_days : typing.Optional[int] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Agent - The created agent. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.create( - name="name", - voice_id="voice_id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/agents", - method="POST", - json={ - "name": name, - "slug": slug, - "prompt": prompt, - "first_message": first_message, - "language": language, - "llm_model": llm_model, - "voice_id": voice_id, - "temperature": temperature, - "config": config, - "is_public": is_public, - "allowed_origins": allowed_origins, - "hostname_allowlist": hostname_allowlist, - "memory_enabled": memory_enabled, - "memory_retention_days": memory_retention_days, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - Agent, - parse_obj_as( - type_=Agent, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def get(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> Agent: - """ - Retrieve a voice agent by ID. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Agent - The requested agent. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.get( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - Agent, - parse_obj_as( - type_=Agent, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def delete(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: - """ - Delete a voice agent. Conversations and attached tools remain. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.delete( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def update( - self, - id: str, - *, - name: typing.Optional[str] = OMIT, - prompt: typing.Optional[str] = OMIT, - first_message: typing.Optional[str] = OMIT, - language: typing.Optional[str] = OMIT, - llm_model: typing.Optional[str] = OMIT, - voice_id: typing.Optional[str] = OMIT, - temperature: typing.Optional[float] = OMIT, - config: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = OMIT, - is_public: typing.Optional[bool] = OMIT, - allowed_origins: typing.Optional[typing.Sequence[str]] = OMIT, - hostname_allowlist: typing.Optional[typing.Sequence[str]] = OMIT, - memory_enabled: typing.Optional[bool] = OMIT, - memory_retention_days: typing.Optional[int] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> Agent: - """ - Update a voice agent. Only fields present on the request body are changed. - - Parameters - ---------- - id : str - - name : typing.Optional[str] - - prompt : typing.Optional[str] - - first_message : typing.Optional[str] - - language : typing.Optional[str] - - llm_model : typing.Optional[str] - - voice_id : typing.Optional[str] - - temperature : typing.Optional[float] - - config : typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] - - is_public : typing.Optional[bool] - - allowed_origins : typing.Optional[typing.Sequence[str]] - - hostname_allowlist : typing.Optional[typing.Sequence[str]] - When supplied, replaces the stored list. Pass an empty - array to clear enforcement (public agent is open again). - Omit the field to leave the existing value unchanged. - - memory_enabled : typing.Optional[bool] - - memory_retention_days : typing.Optional[int] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Agent - The updated agent. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.update( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}", - method="PATCH", - json={ - "name": name, - "prompt": prompt, - "first_message": first_message, - "language": language, - "llm_model": llm_model, - "voice_id": voice_id, - "temperature": temperature, - "config": config, - "is_public": is_public, - "allowed_origins": allowed_origins, - "hostname_allowlist": hostname_allowlist, - "memory_enabled": memory_enabled, - "memory_retention_days": memory_retention_days, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - Agent, - parse_obj_as( - type_=Agent, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def list_tools( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> ListToolsResponse: - """ - List tools currently attached to the agent. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListToolsResponse - Attached tools for the agent. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.list_tools( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}/tools", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListToolsResponse, - parse_obj_as( - type_=ListToolsResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def attach_tool( - self, id: str, tool_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> None: - """ - Attach an existing tool to the agent so the LLM can call it. - - Parameters - ---------- - id : str - - tool_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.attach_tool( - id="id", - tool_id="toolId", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}/tools/{jsonable_encoder(tool_id)}", - method="POST", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def detach_tool( - self, id: str, tool_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> None: - """ - Detach a tool from the agent. - - Parameters - ---------- - id : str - - tool_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.detach_tool( - id="id", - tool_id="toolId", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}/tools/{jsonable_encoder(tool_id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def get_evaluation_config( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> EvaluationConfig: - """ - Retrieve the agent's post-call evaluation criteria + data-collection config. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - EvaluationConfig - The evaluation config for the agent. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.get_evaluation_config( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}/evaluation-config", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - EvaluationConfig, - parse_obj_as( - type_=EvaluationConfig, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def update_evaluation_config( - self, - id: str, - *, - criteria: typing.Sequence[EvaluationCriterion], - data_collection: typing.Sequence[DataCollectionField], - request_options: typing.Optional[RequestOptions] = None, - ) -> EvaluationConfig: - """ - Replace the agent's evaluation criteria + data-collection fields. - - Parameters - ---------- - id : str - - criteria : typing.Sequence[EvaluationCriterion] - - data_collection : typing.Sequence[DataCollectionField] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - EvaluationConfig - The updated evaluation config. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - from speechify.tts import DataCollectionField, EvaluationCriterion - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.update_evaluation_config( - id="id", - criteria=[ - EvaluationCriterion( - id="id", - name="name", - description="description", - ) - ], - data_collection=[ - DataCollectionField( - key="key", - description="description", - type="string", - ) - ], - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}/evaluation-config", - method="PATCH", - json={ - "criteria": convert_and_respect_annotation_metadata( - object_=criteria, annotation=typing.Sequence[EvaluationCriterion], direction="write" - ), - "data_collection": convert_and_respect_annotation_metadata( - object_=data_collection, annotation=typing.Sequence[DataCollectionField], direction="write" - ), - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - EvaluationConfig, - parse_obj_as( - type_=EvaluationConfig, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def get_dynamic_variables( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> ListDynamicVariablesResponse: - """ - Retrieve the agent's customer-scope dynamic variables and the read-only - catalogue of reserved `system__*` keys. The system variables list is - provided so editor UIs can render the reference list without maintaining - a client-side copy of the catalogue. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListDynamicVariablesResponse - The agent's variable catalogue. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.get_dynamic_variables( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}/variables", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListDynamicVariablesResponse, - parse_obj_as( - type_=ListDynamicVariablesResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def update_dynamic_variables( - self, - id: str, - *, - variables: typing.Sequence[DynamicVariable], - request_options: typing.Optional[RequestOptions] = None, - ) -> ListDynamicVariablesResponse: - """ - Replace the agent's customer-scope dynamic variable definitions. - The supplied list overwrites the stored list wholesale (same - semantics as `updateEvaluationConfig`). Pass an empty array to - clear all variables. Up to 20 variables per agent. Keys must - match `[a-zA-Z0-9_]+` and must not start with the reserved - `system__` prefix. - - Parameters - ---------- - id : str - - variables : typing.Sequence[DynamicVariable] - The new variable list. Replaces the existing list entirely. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListDynamicVariablesResponse - The updated variable catalogue. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - from speechify.tts import DynamicVariable - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.update_dynamic_variables( - id="id", - variables=[ - DynamicVariable( - key="product_name", - type="string", - default="Speechify", - description="Product the agent is supporting.", - ), - DynamicVariable( - key="support_tier", - type="number", - default=1, - ), - DynamicVariable( - key="account_metadata", - type="json", - description="Arbitrary account context injected into tool bodies.", - ), - ], - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}/variables", - method="PATCH", - json={ - "variables": convert_and_respect_annotation_metadata( - object_=variables, annotation=typing.Sequence[DynamicVariable], direction="write" - ), - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListDynamicVariablesResponse, - parse_obj_as( - type_=ListDynamicVariablesResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def create_conversation( - self, - id: str, - *, - transport: typing.Optional[str] = OMIT, - dynamic_variables: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> CreateConversationResponse: - """ - Start a new voice conversation with the agent. Returns a realtime - voice session + short-lived client token so the caller can - connect the audio pipeline directly. The agent is dispatched - server-side; no additional client action required. - - Pass `dynamic_variables` to supply per-session values that override - the agent's stored variable defaults for this one conversation. - Keys in the `system__` namespace are rejected at this boundary. - - Parameters - ---------- - id : str - - transport : typing.Optional[str] - Transport hint. Omit to use the agent's default. - - dynamic_variables : typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] - Per-session variable overrides that merge on top of the agent's - stored variable defaults for this one conversation. Keys in the - reserved `system__` namespace are rejected. Values must match the - declared type of the corresponding variable definition on the agent. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - CreateConversationResponse - The created conversation with its realtime session token. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.create_conversation( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}/conversations", - method="POST", - json={ - "transport": transport, - "dynamic_variables": dynamic_variables, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - CreateConversationResponse, - parse_obj_as( - type_=CreateConversationResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def create_session( - self, - id: str, - *, - user_identity: typing.Optional[str] = OMIT, - dynamic_variables: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> CreateConversationResponse: - """ - Mint a realtime voice session for the given agent. Widget-friendly - counterpart to `createConversation` — same response shape, dual - authentication: - - * **Authenticated (Bearer)**: works for any agent the caller - owns. Typical server-to-server flow where the embedding - site's backend mints a token and hands it to the browser so - the API key never reaches the client. - * **Unauthenticated**: works only when `agent.is_public = true` - AND the request's `Origin` header matches `agent.allowed_origins` - (or that list is empty). When `agent.hostname_allowlist` is - non-empty, the `Origin` hostname must additionally be a - member of that list. Used directly by the - `` web component. - - Responds with the same `CreateConversationResponse` as - `createConversation`. - - Parameters - ---------- - id : str - - user_identity : typing.Optional[str] - Opaque identifier for the end-user (e.g. your app's user ID). Stamped onto the conversation. Optional - defaults to an anonymous per-session ID. - - dynamic_variables : typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] - Per-session variable overrides that merge on top of the agent's - stored variable defaults for this one session. Keys in the - reserved `system__` namespace are rejected at this boundary. - Values must match the declared type of the corresponding variable - definition on the agent (a `string` type expects a JSON string, - `number` expects a JSON number, etc.). - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - CreateConversationResponse - The created session with its realtime token + URL. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.create_session( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}/sessions", - method="POST", - json={ - "user_identity": user_identity, - "dynamic_variables": dynamic_variables, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - CreateConversationResponse, - parse_obj_as( - type_=CreateConversationResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 403: - raise ForbiddenError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def list_agent_knowledge_bases( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> ListKnowledgeBasesResponse: - """ - List knowledge bases attached to an agent. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListKnowledgeBasesResponse - The knowledge bases attached to the agent. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.list_agent_knowledge_bases( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}/knowledge-bases", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListKnowledgeBasesResponse, - parse_obj_as( - type_=ListKnowledgeBasesResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def attach_knowledge_base( - self, id: str, kb_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> None: - """ - Attach a knowledge base to an agent. The `search_knowledge` tool - is auto-registered on the next conversation and can only query the - attached knowledge bases. - - Parameters - ---------- - id : str - - kb_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.attach_knowledge_base( - id="id", - kb_id="kbId", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}/knowledge-bases/{jsonable_encoder(kb_id)}", - method="POST", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def detach_knowledge_base( - self, id: str, kb_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> None: - """ - Detach a knowledge base from an agent. - - Parameters - ---------- - id : str - - kb_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.detach_knowledge_base( - id="id", - kb_id="kbId", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}/knowledge-bases/{jsonable_encoder(kb_id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def list_memories( - self, - id: str, - *, - limit: typing.Optional[int] = None, - offset: typing.Optional[int] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> ListMemoriesResponse: - """ - List per-caller memories extracted for an agent. Memories are - written post-call by the built-in extractor when `memory_enabled` - is true on the agent; the list is sorted newest-first. - - Parameters - ---------- - id : str - - limit : typing.Optional[int] - Maximum rows to return. Defaults to 100, capped at 200. - - offset : typing.Optional[int] - Number of rows to skip. Combine with `limit` to page through older memories. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListMemoriesResponse - Memories for the agent. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.list_memories( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}/memories", - method="GET", - params={ - "limit": limit, - "offset": offset, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListMemoriesResponse, - parse_obj_as( - type_=ListMemoriesResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def delete_memories_by_caller( - self, id: str, *, agent_id: str, caller_identity: str, request_options: typing.Optional[RequestOptions] = None - ) -> DeleteMemoriesByCallerResponse: - """ - Delete every memory ever extracted for a specific caller on - this agent. Privacy / GDPR surface. Returns the count of rows - soft-deleted; rows become permanently unreachable immediately - and are hard-deleted by the retention job after the tenant's - configured retention window. - - Parameters - ---------- - id : str - - agent_id : str - - caller_identity : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - DeleteMemoriesByCallerResponse - Deletion summary. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.delete_memories_by_caller( - id="id", - agent_id="agent_id", - caller_identity="caller_identity", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}/memories/delete-by-caller", - method="POST", - json={ - "agent_id": agent_id, - "caller_identity": caller_identity, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - DeleteMemoriesByCallerResponse, - parse_obj_as( - type_=DeleteMemoriesByCallerResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def list_tests( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> ListAgentTestsResponse: - """ - List all tests configured for the agent. Each entry includes the - most recent run so the console can render pass/fail badges without - an extra round-trip. - - Parameters - ---------- - id : str - Agent ID. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListAgentTestsResponse - Tests for the agent with last-run summaries. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.list_tests( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}/tests", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListAgentTestsResponse, - parse_obj_as( - type_=ListAgentTestsResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def create_test( - self, - id: str, - *, - name: str, - type: TestType, - config: CreateAgentTestRequestConfig, - description: typing.Optional[str] = OMIT, - tool_mock_config: typing.Optional[ToolMockConfig] = OMIT, - variables: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = OMIT, - folder_id: typing.Optional[str] = OMIT, - attached_agent_ids: typing.Optional[typing.Sequence[str]] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> AgentTest: - """ - Create a new test for the agent. - - Parameters - ---------- - id : str - Agent ID. - - name : str - Short human-readable label for the test. - - type : TestType - - config : CreateAgentTestRequestConfig - Type-specific configuration. Must match the shape for the given `type`. - - description : typing.Optional[str] - Optional longer description of what this test verifies. - - tool_mock_config : typing.Optional[ToolMockConfig] - Optional tool-mocking config applied during every run of this test. - - variables : typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] - Per-test variable values substituted into string fields of the - config at run-start. Keys use the same rules as agent-level - `DynamicVariable` keys. - - folder_id : typing.Optional[str] - Folder to place the test in. Omit for root. - - attached_agent_ids : typing.Optional[typing.Sequence[str]] - Optional list of additional agents this test should also run - against. The owner agent (path param) is always attached - implicitly. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AgentTest - The created test. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - from speechify.tts import ScenarioConfig - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.create_test( - id="id", - name="Greet the caller by name", - description="Agent should greet the caller using their name when provided.", - type="scenario", - config=ScenarioConfig( - context="The caller says: Hi, I'm Alice.", - success_criteria="The agent greets Alice by name.", - success_examples=["Hi Alice! How can I help you today?"], - failure_examples=["Hello! How can I help you?"], - ), - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}/tests", - method="POST", - json={ - "name": name, - "description": description, - "type": type, - "config": convert_and_respect_annotation_metadata( - object_=config, annotation=CreateAgentTestRequestConfig, direction="write" - ), - "tool_mock_config": convert_and_respect_annotation_metadata( - object_=tool_mock_config, annotation=ToolMockConfig, direction="write" - ), - "variables": variables, - "folder_id": folder_id, - "attached_agent_ids": attached_agent_ids, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - AgentTest, - parse_obj_as( - type_=AgentTest, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def run_all_tests( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> RunAgentTestsResponse: - """ - Enqueue runs for every test on the agent concurrently. Up to 50 - tests are dispatched in one call. Each returned run starts in - `queued` status; poll `GET /v1/test-runs/{id}` for the terminal - result. - - Parameters - ---------- - id : str - Agent ID. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - RunAgentTestsResponse - Queued runs for all tests on the agent. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.run_all_tests( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/agents/{jsonable_encoder(id)}/tests/runs", - method="POST", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - RunAgentTestsResponse, - parse_obj_as( - type_=RunAgentTestsResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def get_test(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> AgentTest: - """ - Retrieve a test by ID. - - Parameters - ---------- - id : str - Test ID. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AgentTest - The requested test. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.get_test( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/tests/{jsonable_encoder(id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - AgentTest, - parse_obj_as( - type_=AgentTest, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def delete_test(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: - """ - Delete a test and all its run history. - - Parameters - ---------- - id : str - Test ID. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.delete_test( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/tests/{jsonable_encoder(id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def update_test( - self, - id: str, - *, - name: typing.Optional[str] = OMIT, - description: typing.Optional[str] = OMIT, - config: typing.Optional[UpdateAgentTestRequestConfig] = OMIT, - tool_mock_config: typing.Optional[ToolMockConfig] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> AgentTest: - """ - Update a test. Only fields present on the request body are changed. - - Parameters - ---------- - id : str - Test ID. - - name : typing.Optional[str] - - description : typing.Optional[str] - - config : typing.Optional[UpdateAgentTestRequestConfig] - Replaces the test config when present. - - tool_mock_config : typing.Optional[ToolMockConfig] - Replaces the tool-mock config when present. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AgentTest - The updated test. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.update_test( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/tests/{jsonable_encoder(id)}", - method="PATCH", - json={ - "name": name, - "description": description, - "config": convert_and_respect_annotation_metadata( - object_=config, annotation=UpdateAgentTestRequestConfig, direction="write" - ), - "tool_mock_config": convert_and_respect_annotation_metadata( - object_=tool_mock_config, annotation=ToolMockConfig, direction="write" - ), - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - AgentTest, - parse_obj_as( - type_=AgentTest, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def list_test_runs( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> ListAgentTestRunsResponse: - """ - List the run history for a test, newest first. - - Parameters - ---------- - id : str - Test ID. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListAgentTestRunsResponse - Run history for the test. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.list_test_runs( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/tests/{jsonable_encoder(id)}/runs", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListAgentTestRunsResponse, - parse_obj_as( - type_=ListAgentTestRunsResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def run_test(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> AgentTestRun: - """ - Enqueue a single run of the test. The returned run starts in - `queued` status. Poll `GET /v1/test-runs/{id}` until the status - reaches a terminal state (`passed`, `failed`, or `error`). - - Parameters - ---------- - id : str - Test ID. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AgentTestRun - The queued run. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.run_test( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/tests/{jsonable_encoder(id)}/runs", - method="POST", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - AgentTestRun, - parse_obj_as( - type_=AgentTestRun, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def get_test_run(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> AgentTestRun: - """ - Retrieve a single test run by ID. Poll this endpoint until - `status` reaches a terminal state (`passed`, `failed`, or `error`). - The `result` field is populated on terminal states. - - Parameters - ---------- - id : str - Test run ID. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AgentTestRun - The test run. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.get_test_run( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/test-runs/{jsonable_encoder(id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - AgentTestRun, - parse_obj_as( - type_=AgentTestRun, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def list_all_tests( - self, - *, - agent_id: typing.Optional[str] = None, - type: typing.Optional[str] = None, - status: typing.Optional[str] = None, - folder_id: typing.Optional[str] = None, - updated_after: typing.Optional[str] = None, - q: typing.Optional[str] = None, - limit: typing.Optional[int] = None, - cursor: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> ListTestsResponse: - """ - Workspace-wide list of tests across every agent the caller owns. - Supports filters (agent, type, last-run status, folder), full-text - search on name/description, and cursor pagination. Each row carries - its newest run and attached agent IDs so the list renders without - N+1 round-trips. - - Parameters - ---------- - agent_id : typing.Optional[str] - Comma-separated agent IDs to filter on. - - type : typing.Optional[str] - Comma-separated test types (scenario|tool|simulation). - - status : typing.Optional[str] - Comma-separated last-run statuses. - - folder_id : typing.Optional[str] - Folder ID to filter on, or "root" for unfiled tests. - - updated_after : typing.Optional[str] - Only return tests updated after this RFC3339 timestamp. - - q : typing.Optional[str] - Substring match on name or description. - - limit : typing.Optional[int] - Max tests per page (default 50, max 200). - - cursor : typing.Optional[str] - Opaque pagination cursor from a previous response. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListTestsResponse - Paginated list. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.list_all_tests() - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/tests", - method="GET", - params={ - "agent_id": agent_id, - "type": type, - "status": status, - "folder_id": folder_id, - "updated_after": updated_after, - "q": q, - "limit": limit, - "cursor": cursor, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListTestsResponse, - parse_obj_as( - type_=ListTestsResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def get_test_stats( - self, *, window_days: typing.Optional[int] = None, request_options: typing.Optional[RequestOptions] = None - ) -> TestStats: - """ - Aggregate pass-rate metrics over the last N days. Returns dense - daily buckets (one entry per day, zero-filled) plus totals and a - per-type breakdown. Powers the header chart on the global tests - page. Default window is 30 days, max 90. - - Parameters - ---------- - window_days : typing.Optional[int] - Trailing window in days (default 30, max 90). - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - TestStats - Stats payload. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.get_test_stats() - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/tests/stats", - method="GET", - params={ - "window_days": window_days, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - TestStats, - parse_obj_as( - type_=TestStats, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def run_tests_batch( - self, *, entries: typing.Sequence[BatchRunEntry], request_options: typing.Optional[RequestOptions] = None - ) -> RunBatchResponse: - """ - Queue runs for every (test, agent) pair in the body. Entries - without an `agent_id` fan out to every agent the test is - attached to. Total expanded runs are capped at 100 per call. - Each entry in the response is a queued run; poll - `GET /v1/test-runs/{id}` for each. - - Parameters - ---------- - entries : typing.Sequence[BatchRunEntry] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - RunBatchResponse - Runs queued. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - from speechify.tts import BatchRunEntry - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.run_tests_batch( - entries=[ - BatchRunEntry( - test_id="test_id", - ) - ], - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/tests/runs:batch", - method="POST", - json={ - "entries": convert_and_respect_annotation_metadata( - object_=entries, annotation=typing.Sequence[BatchRunEntry], direction="write" - ), - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - RunBatchResponse, - parse_obj_as( - type_=RunBatchResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def list_test_attachments( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> ListAgentTestAttachmentsResponse: - """ - List every agent a test is attached to. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListAgentTestAttachmentsResponse - Attachment list. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.list_test_attachments( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/tests/{jsonable_encoder(id)}/attachments", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListAgentTestAttachmentsResponse, - parse_obj_as( - type_=ListAgentTestAttachmentsResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def attach_test( - self, id: str, agent_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> None: - """ - Attach a test to an additional agent. After this call, the test - will also run as part of that agent's regression suite (and - against its prompt + tool config when invoked with - `agent_id = {agentId}`). Idempotent. - - Parameters - ---------- - id : str - - agent_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.attach_test( - id="id", - agent_id="agentId", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/tests/{jsonable_encoder(id)}/attachments/{jsonable_encoder(agent_id)}", - method="POST", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def detach_test( - self, id: str, agent_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> None: - """ - Detach a test from an agent. The owner agent (the agent the test - was authored against) cannot be detached; delete the test - instead. - - Parameters - ---------- - id : str - - agent_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.detach_test( - id="id", - agent_id="agentId", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/tests/{jsonable_encoder(id)}/attachments/{jsonable_encoder(agent_id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def move_test( - self, - id: str, - *, - folder_id: typing.Optional[str] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> None: - """ - Move a test into a folder. Pass `folder_id: null` for root. - - Parameters - ---------- - id : str - - folder_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.move_test( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/tests/{jsonable_encoder(id)}/move", - method="POST", - json={ - "folder_id": folder_id, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def list_test_folders( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> ListAgentTestFoldersResponse: - """ - List every test folder the caller owns. Flat list; build the tree client-side. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListAgentTestFoldersResponse - Folder list. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.list_test_folders() - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/test-folders", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListAgentTestFoldersResponse, - parse_obj_as( - type_=ListAgentTestFoldersResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def create_test_folder( - self, - *, - name: str, - parent_folder_id: typing.Optional[str] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> AgentTestFolder: - """ - Create a test folder. Max depth is 3. - - Parameters - ---------- - name : str - - parent_folder_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AgentTestFolder - Created folder. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.create_test_folder( - name="name", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/test-folders", - method="POST", - json={ - "name": name, - "parent_folder_id": parent_folder_id, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - AgentTestFolder, - parse_obj_as( - type_=AgentTestFolder, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def delete_test_folder(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: - """ - Soft-delete a folder. Child tests drop back to root. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.delete_test_folder( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/test-folders/{jsonable_encoder(id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def update_test_folder( - self, - id: str, - *, - name: typing.Optional[str] = OMIT, - parent_folder_id: typing.Optional[str] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> AgentTestFolder: - """ - Rename or reparent a test folder. Cycles are rejected. - - Parameters - ---------- - id : str - - name : typing.Optional[str] - - parent_folder_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AgentTestFolder - Updated folder. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.agents.update_test_folder( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/test-folders/{jsonable_encoder(id)}", - method="PATCH", - json={ - "name": name, - "parent_folder_id": parent_folder_id, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - AgentTestFolder, - parse_obj_as( - type_=AgentTestFolder, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/src/speechify/tts/agents/types/__init__.py b/src/speechify/tts/agents/types/__init__.py deleted file mode 100644 index 6b02c60..0000000 --- a/src/speechify/tts/agents/types/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .create_agent_test_request_config import CreateAgentTestRequestConfig -from .update_agent_test_request_config import UpdateAgentTestRequestConfig - -__all__ = ["CreateAgentTestRequestConfig", "UpdateAgentTestRequestConfig"] diff --git a/src/speechify/tts/agents/types/create_agent_test_request_config.py b/src/speechify/tts/agents/types/create_agent_test_request_config.py deleted file mode 100644 index b2d407d..0000000 --- a/src/speechify/tts/agents/types/create_agent_test_request_config.py +++ /dev/null @@ -1,8 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from ...types.scenario_config import ScenarioConfig -from ...types.tool_call_config import ToolCallConfig -from ...types.simulation_config import SimulationConfig - -CreateAgentTestRequestConfig = typing.Union[ScenarioConfig, ToolCallConfig, SimulationConfig] diff --git a/src/speechify/tts/agents/types/update_agent_test_request_config.py b/src/speechify/tts/agents/types/update_agent_test_request_config.py deleted file mode 100644 index 67638a8..0000000 --- a/src/speechify/tts/agents/types/update_agent_test_request_config.py +++ /dev/null @@ -1,8 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from ...types.scenario_config import ScenarioConfig -from ...types.tool_call_config import ToolCallConfig -from ...types.simulation_config import SimulationConfig - -UpdateAgentTestRequestConfig = typing.Union[ScenarioConfig, ToolCallConfig, SimulationConfig] diff --git a/src/speechify/tts/audio/__init__.py b/src/speechify/tts/audio/__init__.py deleted file mode 100644 index 31d198a..0000000 --- a/src/speechify/tts/audio/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .types import GetSpeechRequestAudioFormat, GetSpeechRequestModel, GetStreamRequestModel, StreamAudioRequestAccept - -__all__ = ["GetSpeechRequestAudioFormat", "GetSpeechRequestModel", "GetStreamRequestModel", "StreamAudioRequestAccept"] diff --git a/src/speechify/tts/audio/client.py b/src/speechify/tts/audio/client.py deleted file mode 100644 index 055a5d8..0000000 --- a/src/speechify/tts/audio/client.py +++ /dev/null @@ -1,607 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from ...core.client_wrapper import SyncClientWrapper -from .types.get_speech_request_audio_format import GetSpeechRequestAudioFormat -from .types.get_speech_request_model import GetSpeechRequestModel -from ..types.get_speech_options_request import GetSpeechOptionsRequest -from ...core.request_options import RequestOptions -from ..types.get_speech_response import GetSpeechResponse -from ...core.serialization import convert_and_respect_annotation_metadata -from ...core.pydantic_utilities import parse_obj_as -from ..errors.bad_request_error import BadRequestError -from ..errors.unauthorized_error import UnauthorizedError -from ..errors.payment_required_error import PaymentRequiredError -from ..errors.forbidden_error import ForbiddenError -from ..errors.internal_server_error import InternalServerError -from json.decoder import JSONDecodeError -from ...core.api_error import ApiError -from .types.stream_audio_request_accept import StreamAudioRequestAccept -from .types.get_stream_request_model import GetStreamRequestModel -from ..types.get_stream_options_request import GetStreamOptionsRequest -from ...core.client_wrapper import AsyncClientWrapper - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class AudioClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def speech( - self, - *, - input: str, - voice_id: str, - audio_format: typing.Optional[GetSpeechRequestAudioFormat] = OMIT, - language: typing.Optional[str] = OMIT, - model: typing.Optional[GetSpeechRequestModel] = OMIT, - options: typing.Optional[GetSpeechOptionsRequest] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> GetSpeechResponse: - """ - Gets the speech data for the given input - - Parameters - ---------- - input : str - Plain text or SSML to be synthesized to speech. - Refer to https://docs.speechify.ai/docs/api-limits for the input size limits. - Emotion, Pitch and Speed Rate are configured in the ssml input, please refer to the ssml documentation for more information: https://docs.speechify.ai/docs/ssml#prosody - - voice_id : str - Id of the voice to be used for synthesizing speech. Refer to /v1/voices endpoint for available voices - - audio_format : typing.Optional[GetSpeechRequestAudioFormat] - The format for the output audio. Note, that the current default is "wav", but there's no guarantee it will not change in the future. We recommend always passing the specific param you expect. - - language : typing.Optional[str] - Language of the input. Follow the format of an ISO 639-1 language code and an ISO 3166-1 region code, separated by a hyphen, e.g. en-US. - Please refer to the list of the supported languages and recommendations regarding this parameter: https://docs.speechify.ai/docs/language-support. - - model : typing.Optional[GetSpeechRequestModel] - Model used for audio synthesis. `simba-base` and `simba-turbo` are deprecated. - `simba-3.0` is the new streaming-native model with lower TTFB and richer expressivity. Currently English only; multilingual coming soon. Non-English voices return 400 until multilingual support ships. - - options : typing.Optional[GetSpeechOptionsRequest] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - GetSpeechResponse - - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.audio.speech( - input="input", - voice_id="voice_id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - "v1/audio/speech", - method="POST", - json={ - "audio_format": audio_format, - "input": input, - "language": language, - "model": model, - "options": convert_and_respect_annotation_metadata( - object_=options, annotation=GetSpeechOptionsRequest, direction="write" - ), - "voice_id": voice_id, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - GetSpeechResponse, - parse_obj_as( - type_=GetSpeechResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 402: - raise PaymentRequiredError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 403: - raise ForbiddenError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 500: - raise InternalServerError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def stream( - self, - *, - accept: StreamAudioRequestAccept, - input: str, - voice_id: str, - language: typing.Optional[str] = OMIT, - model: typing.Optional[GetStreamRequestModel] = OMIT, - options: typing.Optional[GetStreamOptionsRequest] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> typing.Iterator[bytes]: - """ - Gets the stream speech for the given input - - Parameters - ---------- - accept : StreamAudioRequestAccept - - input : str - Plain text or SSML to be synthesized to speech. - Refer to https://docs.speechify.ai/docs/api-limits for the input size limits. - Emotion, Pitch and Speed Rate are configured in the ssml input, please refer to the ssml documentation for more information: https://docs.speechify.ai/docs/ssml#prosody - - voice_id : str - Id of the voice to be used for synthesizing speech. Refer to /v1/voices endpoint for available voices - - language : typing.Optional[str] - Language of the input. Follow the format of an ISO 639-1 language code and an ISO 3166-1 region code, separated by a hyphen, e.g. en-US. - Please refer to the list of the supported languages and recommendations regarding this parameter: https://docs.speechify.ai/docs/language-support. - - model : typing.Optional[GetStreamRequestModel] - Model used for audio synthesis. `simba-base` and `simba-turbo` are deprecated. - `simba-3.0` is the new streaming-native model with lower TTFB and richer expressivity. Currently English only; multilingual coming soon. Non-English voices return 400 until multilingual support ships. - - options : typing.Optional[GetStreamOptionsRequest] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. You can pass in configuration such as `chunk_size`, and more to customize the request and response. - - Yields - ------ - typing.Iterator[bytes] - - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.audio.stream( - accept="audio/mpeg", - input="input", - voice_id="voice_id", - ) - """ - with self._client_wrapper.httpx_client.stream( - "v1/audio/stream", - method="POST", - json={ - "input": input, - "language": language, - "model": model, - "options": convert_and_respect_annotation_metadata( - object_=options, annotation=GetStreamOptionsRequest, direction="write" - ), - "voice_id": voice_id, - }, - headers={ - "content-type": "application/json", - "Accept": str(accept) if accept is not None else None, - }, - request_options=request_options, - omit=OMIT, - ) as _response: - try: - if 200 <= _response.status_code < 300: - _chunk_size = request_options.get("chunk_size", None) if request_options is not None else None - for _chunk in _response.iter_bytes(chunk_size=_chunk_size): - yield _chunk - return - _response.read() - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 402: - raise PaymentRequiredError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 403: - raise ForbiddenError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 500: - raise InternalServerError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - -class AsyncAudioClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def speech( - self, - *, - input: str, - voice_id: str, - audio_format: typing.Optional[GetSpeechRequestAudioFormat] = OMIT, - language: typing.Optional[str] = OMIT, - model: typing.Optional[GetSpeechRequestModel] = OMIT, - options: typing.Optional[GetSpeechOptionsRequest] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> GetSpeechResponse: - """ - Gets the speech data for the given input - - Parameters - ---------- - input : str - Plain text or SSML to be synthesized to speech. - Refer to https://docs.speechify.ai/docs/api-limits for the input size limits. - Emotion, Pitch and Speed Rate are configured in the ssml input, please refer to the ssml documentation for more information: https://docs.speechify.ai/docs/ssml#prosody - - voice_id : str - Id of the voice to be used for synthesizing speech. Refer to /v1/voices endpoint for available voices - - audio_format : typing.Optional[GetSpeechRequestAudioFormat] - The format for the output audio. Note, that the current default is "wav", but there's no guarantee it will not change in the future. We recommend always passing the specific param you expect. - - language : typing.Optional[str] - Language of the input. Follow the format of an ISO 639-1 language code and an ISO 3166-1 region code, separated by a hyphen, e.g. en-US. - Please refer to the list of the supported languages and recommendations regarding this parameter: https://docs.speechify.ai/docs/language-support. - - model : typing.Optional[GetSpeechRequestModel] - Model used for audio synthesis. `simba-base` and `simba-turbo` are deprecated. - `simba-3.0` is the new streaming-native model with lower TTFB and richer expressivity. Currently English only; multilingual coming soon. Non-English voices return 400 until multilingual support ships. - - options : typing.Optional[GetSpeechOptionsRequest] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - GetSpeechResponse - - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.audio.speech( - input="input", - voice_id="voice_id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/audio/speech", - method="POST", - json={ - "audio_format": audio_format, - "input": input, - "language": language, - "model": model, - "options": convert_and_respect_annotation_metadata( - object_=options, annotation=GetSpeechOptionsRequest, direction="write" - ), - "voice_id": voice_id, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - GetSpeechResponse, - parse_obj_as( - type_=GetSpeechResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 402: - raise PaymentRequiredError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 403: - raise ForbiddenError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 500: - raise InternalServerError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def stream( - self, - *, - accept: StreamAudioRequestAccept, - input: str, - voice_id: str, - language: typing.Optional[str] = OMIT, - model: typing.Optional[GetStreamRequestModel] = OMIT, - options: typing.Optional[GetStreamOptionsRequest] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> typing.AsyncIterator[bytes]: - """ - Gets the stream speech for the given input - - Parameters - ---------- - accept : StreamAudioRequestAccept - - input : str - Plain text or SSML to be synthesized to speech. - Refer to https://docs.speechify.ai/docs/api-limits for the input size limits. - Emotion, Pitch and Speed Rate are configured in the ssml input, please refer to the ssml documentation for more information: https://docs.speechify.ai/docs/ssml#prosody - - voice_id : str - Id of the voice to be used for synthesizing speech. Refer to /v1/voices endpoint for available voices - - language : typing.Optional[str] - Language of the input. Follow the format of an ISO 639-1 language code and an ISO 3166-1 region code, separated by a hyphen, e.g. en-US. - Please refer to the list of the supported languages and recommendations regarding this parameter: https://docs.speechify.ai/docs/language-support. - - model : typing.Optional[GetStreamRequestModel] - Model used for audio synthesis. `simba-base` and `simba-turbo` are deprecated. - `simba-3.0` is the new streaming-native model with lower TTFB and richer expressivity. Currently English only; multilingual coming soon. Non-English voices return 400 until multilingual support ships. - - options : typing.Optional[GetStreamOptionsRequest] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. You can pass in configuration such as `chunk_size`, and more to customize the request and response. - - Yields - ------ - typing.AsyncIterator[bytes] - - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.audio.stream( - accept="audio/mpeg", - input="input", - voice_id="voice_id", - ) - - - asyncio.run(main()) - """ - async with self._client_wrapper.httpx_client.stream( - "v1/audio/stream", - method="POST", - json={ - "input": input, - "language": language, - "model": model, - "options": convert_and_respect_annotation_metadata( - object_=options, annotation=GetStreamOptionsRequest, direction="write" - ), - "voice_id": voice_id, - }, - headers={ - "content-type": "application/json", - "Accept": str(accept) if accept is not None else None, - }, - request_options=request_options, - omit=OMIT, - ) as _response: - try: - if 200 <= _response.status_code < 300: - _chunk_size = request_options.get("chunk_size", None) if request_options is not None else None - async for _chunk in _response.aiter_bytes(chunk_size=_chunk_size): - yield _chunk - return - await _response.aread() - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 402: - raise PaymentRequiredError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 403: - raise ForbiddenError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 500: - raise InternalServerError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/src/speechify/tts/audio/types/__init__.py b/src/speechify/tts/audio/types/__init__.py deleted file mode 100644 index bda2df6..0000000 --- a/src/speechify/tts/audio/types/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .get_speech_request_audio_format import GetSpeechRequestAudioFormat -from .get_speech_request_model import GetSpeechRequestModel -from .get_stream_request_model import GetStreamRequestModel -from .stream_audio_request_accept import StreamAudioRequestAccept - -__all__ = ["GetSpeechRequestAudioFormat", "GetSpeechRequestModel", "GetStreamRequestModel", "StreamAudioRequestAccept"] diff --git a/src/speechify/tts/audio/types/get_speech_request_model.py b/src/speechify/tts/audio/types/get_speech_request_model.py deleted file mode 100644 index 6ff3461..0000000 --- a/src/speechify/tts/audio/types/get_speech_request_model.py +++ /dev/null @@ -1,7 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -GetSpeechRequestModel = typing.Union[ - typing.Literal["simba-base", "simba-english", "simba-multilingual", "simba-turbo", "simba-3.0"], typing.Any -] diff --git a/src/speechify/tts/audio/types/get_stream_request_model.py b/src/speechify/tts/audio/types/get_stream_request_model.py deleted file mode 100644 index af9c4b3..0000000 --- a/src/speechify/tts/audio/types/get_stream_request_model.py +++ /dev/null @@ -1,7 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -GetStreamRequestModel = typing.Union[ - typing.Literal["simba-base", "simba-english", "simba-multilingual", "simba-turbo", "simba-3.0"], typing.Any -] diff --git a/src/speechify/tts/auth/__init__.py b/src/speechify/tts/auth/__init__.py deleted file mode 100644 index b1b7579..0000000 --- a/src/speechify/tts/auth/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .types import CreateAccessTokenRequestGrantType, CreateAccessTokenRequestScope - -__all__ = ["CreateAccessTokenRequestGrantType", "CreateAccessTokenRequestScope"] diff --git a/src/speechify/tts/auth/client.py b/src/speechify/tts/auth/client.py deleted file mode 100644 index d43c9c2..0000000 --- a/src/speechify/tts/auth/client.py +++ /dev/null @@ -1,205 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from ...core.client_wrapper import SyncClientWrapper -from .types.create_access_token_request_grant_type import CreateAccessTokenRequestGrantType -from .types.create_access_token_request_scope import CreateAccessTokenRequestScope -from ...core.request_options import RequestOptions -from ..types.access_token import AccessToken -from ...core.pydantic_utilities import parse_obj_as -from ..errors.bad_request_error import BadRequestError -from ..errors.unauthorized_error import UnauthorizedError -from json.decoder import JSONDecodeError -from ...core.api_error import ApiError -from ...core.client_wrapper import AsyncClientWrapper - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class AuthClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def create_access_token( - self, - *, - grant_type: CreateAccessTokenRequestGrantType, - scope: typing.Optional[CreateAccessTokenRequestScope] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> AccessToken: - """ - WARNING: This endpoint is deprecated. Create a new API token for the logged in user. - - Parameters - ---------- - grant_type : CreateAccessTokenRequestGrantType - in: body - - scope : typing.Optional[CreateAccessTokenRequestScope] - The scope, or a space-delimited list of scopes the token is requested for - in: body - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AccessToken - Contains the details of the token which can be used by the user to access the API - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.auth.create_access_token( - grant_type="client_credentials", - ) - """ - _response = self._client_wrapper.httpx_client.request( - "v1/auth/token", - method="POST", - json={ - "grant_type": grant_type, - "scope": scope, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - AccessToken, - parse_obj_as( - type_=AccessToken, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - -class AsyncAuthClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def create_access_token( - self, - *, - grant_type: CreateAccessTokenRequestGrantType, - scope: typing.Optional[CreateAccessTokenRequestScope] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> AccessToken: - """ - WARNING: This endpoint is deprecated. Create a new API token for the logged in user. - - Parameters - ---------- - grant_type : CreateAccessTokenRequestGrantType - in: body - - scope : typing.Optional[CreateAccessTokenRequestScope] - The scope, or a space-delimited list of scopes the token is requested for - in: body - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AccessToken - Contains the details of the token which can be used by the user to access the API - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.auth.create_access_token( - grant_type="client_credentials", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/auth/token", - method="POST", - json={ - "grant_type": grant_type, - "scope": scope, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - AccessToken, - parse_obj_as( - type_=AccessToken, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/src/speechify/tts/auth/types/__init__.py b/src/speechify/tts/auth/types/__init__.py deleted file mode 100644 index d4ee5b2..0000000 --- a/src/speechify/tts/auth/types/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .create_access_token_request_grant_type import CreateAccessTokenRequestGrantType -from .create_access_token_request_scope import CreateAccessTokenRequestScope - -__all__ = ["CreateAccessTokenRequestGrantType", "CreateAccessTokenRequestScope"] diff --git a/src/speechify/tts/auth/types/create_access_token_request_grant_type.py b/src/speechify/tts/auth/types/create_access_token_request_grant_type.py deleted file mode 100644 index 9d7c254..0000000 --- a/src/speechify/tts/auth/types/create_access_token_request_grant_type.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -CreateAccessTokenRequestGrantType = typing.Union[typing.Literal["client_credentials"], typing.Any] diff --git a/src/speechify/tts/auth/types/create_access_token_request_scope.py b/src/speechify/tts/auth/types/create_access_token_request_scope.py deleted file mode 100644 index 6c7c11c..0000000 --- a/src/speechify/tts/auth/types/create_access_token_request_scope.py +++ /dev/null @@ -1,10 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -CreateAccessTokenRequestScope = typing.Union[ - typing.Literal[ - "audio:speech", "audio:stream", "audio:all", "voices:read", "voices:create", "voices:delete", "voices:all" - ], - typing.Any, -] diff --git a/src/speechify/tts/client.py b/src/speechify/tts/client.py deleted file mode 100644 index 34a8e3c..0000000 --- a/src/speechify/tts/client.py +++ /dev/null @@ -1,62 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ..core.client_wrapper import SyncClientWrapper -from .audio.client import AudioClient -from .auth.client import AuthClient -from .voices.client import VoicesClient -from .agents.client import AgentsClient -from .tools.client import ToolsClient -from .conversations.client import ConversationsClient -from .knowledge_bases.client import KnowledgeBasesClient -from .memories.client import MemoriesClient -from .phone_numbers.client import PhoneNumbersClient -from .sip_trunks.client import SipTrunksClient -from .outbound_calls.client import OutboundCallsClient -from .workspaces.client import WorkspacesClient -from ..core.client_wrapper import AsyncClientWrapper -from .audio.client import AsyncAudioClient -from .auth.client import AsyncAuthClient -from .voices.client import AsyncVoicesClient -from .agents.client import AsyncAgentsClient -from .tools.client import AsyncToolsClient -from .conversations.client import AsyncConversationsClient -from .knowledge_bases.client import AsyncKnowledgeBasesClient -from .memories.client import AsyncMemoriesClient -from .phone_numbers.client import AsyncPhoneNumbersClient -from .sip_trunks.client import AsyncSipTrunksClient -from .outbound_calls.client import AsyncOutboundCallsClient -from .workspaces.client import AsyncWorkspacesClient - - -class TtsClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - self.audio = AudioClient(client_wrapper=self._client_wrapper) - self.auth = AuthClient(client_wrapper=self._client_wrapper) - self.voices = VoicesClient(client_wrapper=self._client_wrapper) - self.agents = AgentsClient(client_wrapper=self._client_wrapper) - self.tools = ToolsClient(client_wrapper=self._client_wrapper) - self.conversations = ConversationsClient(client_wrapper=self._client_wrapper) - self.knowledge_bases = KnowledgeBasesClient(client_wrapper=self._client_wrapper) - self.memories = MemoriesClient(client_wrapper=self._client_wrapper) - self.phone_numbers = PhoneNumbersClient(client_wrapper=self._client_wrapper) - self.sip_trunks = SipTrunksClient(client_wrapper=self._client_wrapper) - self.outbound_calls = OutboundCallsClient(client_wrapper=self._client_wrapper) - self.workspaces = WorkspacesClient(client_wrapper=self._client_wrapper) - - -class AsyncTtsClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - self.audio = AsyncAudioClient(client_wrapper=self._client_wrapper) - self.auth = AsyncAuthClient(client_wrapper=self._client_wrapper) - self.voices = AsyncVoicesClient(client_wrapper=self._client_wrapper) - self.agents = AsyncAgentsClient(client_wrapper=self._client_wrapper) - self.tools = AsyncToolsClient(client_wrapper=self._client_wrapper) - self.conversations = AsyncConversationsClient(client_wrapper=self._client_wrapper) - self.knowledge_bases = AsyncKnowledgeBasesClient(client_wrapper=self._client_wrapper) - self.memories = AsyncMemoriesClient(client_wrapper=self._client_wrapper) - self.phone_numbers = AsyncPhoneNumbersClient(client_wrapper=self._client_wrapper) - self.sip_trunks = AsyncSipTrunksClient(client_wrapper=self._client_wrapper) - self.outbound_calls = AsyncOutboundCallsClient(client_wrapper=self._client_wrapper) - self.workspaces = AsyncWorkspacesClient(client_wrapper=self._client_wrapper) diff --git a/src/speechify/tts/conversations/__init__.py b/src/speechify/tts/conversations/__init__.py deleted file mode 100644 index f3ea265..0000000 --- a/src/speechify/tts/conversations/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - diff --git a/src/speechify/tts/conversations/client.py b/src/speechify/tts/conversations/client.py deleted file mode 100644 index 9fe6ab8..0000000 --- a/src/speechify/tts/conversations/client.py +++ /dev/null @@ -1,824 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.client_wrapper import SyncClientWrapper -import typing -from ...core.request_options import RequestOptions -from ..types.list_conversations_response import ListConversationsResponse -from ...core.pydantic_utilities import parse_obj_as -from ..errors.unauthorized_error import UnauthorizedError -from json.decoder import JSONDecodeError -from ...core.api_error import ApiError -from ..types.conversation_stats import ConversationStats -from ..types.conversation import Conversation -from ...core.jsonable_encoder import jsonable_encoder -from ..errors.not_found_error import NotFoundError -from ..types.list_messages_response import ListMessagesResponse -from ..types.list_evaluations_response import ListEvaluationsResponse -from ..types.list_memories_response import ListMemoriesResponse -from ...core.client_wrapper import AsyncClientWrapper - - -class ConversationsClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> ListConversationsResponse: - """ - List conversations owned by the caller, ordered by most recent. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListConversationsResponse - A list of conversations. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.conversations.list() - """ - _response = self._client_wrapper.httpx_client.request( - "v1/conversations", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListConversationsResponse, - parse_obj_as( - type_=ListConversationsResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def stats(self, *, request_options: typing.Optional[RequestOptions] = None) -> ConversationStats: - """ - Aggregated counts and averages over the caller's conversations, scoped by the same filters as the list endpoint. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ConversationStats - Stats for the matched conversations. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.conversations.stats() - """ - _response = self._client_wrapper.httpx_client.request( - "v1/conversations/stats", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ConversationStats, - parse_obj_as( - type_=ConversationStats, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def get(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> Conversation: - """ - Retrieve a conversation by ID. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Conversation - The requested conversation. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.conversations.get( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/conversations/{jsonable_encoder(id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - Conversation, - parse_obj_as( - type_=Conversation, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def list_messages( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> ListMessagesResponse: - """ - Retrieve the full transcript for a conversation, in order. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListMessagesResponse - The messages for the conversation. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.conversations.list_messages( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/conversations/{jsonable_encoder(id)}/messages", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListMessagesResponse, - parse_obj_as( - type_=ListMessagesResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def list_evaluations( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> ListEvaluationsResponse: - """ - Retrieve post-call evaluation results for a conversation. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListEvaluationsResponse - The evaluations for the conversation. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.conversations.list_evaluations( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/conversations/{jsonable_encoder(id)}/evaluations", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListEvaluationsResponse, - parse_obj_as( - type_=ListEvaluationsResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def list_memories( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> ListMemoriesResponse: - """ - List memories extracted from a specific conversation. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListMemoriesResponse - Memories written during this conversation. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.conversations.list_memories( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/conversations/{jsonable_encoder(id)}/memories", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListMemoriesResponse, - parse_obj_as( - type_=ListMemoriesResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - -class AsyncConversationsClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> ListConversationsResponse: - """ - List conversations owned by the caller, ordered by most recent. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListConversationsResponse - A list of conversations. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.conversations.list() - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/conversations", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListConversationsResponse, - parse_obj_as( - type_=ListConversationsResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def stats(self, *, request_options: typing.Optional[RequestOptions] = None) -> ConversationStats: - """ - Aggregated counts and averages over the caller's conversations, scoped by the same filters as the list endpoint. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ConversationStats - Stats for the matched conversations. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.conversations.stats() - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/conversations/stats", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ConversationStats, - parse_obj_as( - type_=ConversationStats, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def get(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> Conversation: - """ - Retrieve a conversation by ID. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Conversation - The requested conversation. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.conversations.get( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/conversations/{jsonable_encoder(id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - Conversation, - parse_obj_as( - type_=Conversation, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def list_messages( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> ListMessagesResponse: - """ - Retrieve the full transcript for a conversation, in order. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListMessagesResponse - The messages for the conversation. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.conversations.list_messages( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/conversations/{jsonable_encoder(id)}/messages", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListMessagesResponse, - parse_obj_as( - type_=ListMessagesResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def list_evaluations( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> ListEvaluationsResponse: - """ - Retrieve post-call evaluation results for a conversation. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListEvaluationsResponse - The evaluations for the conversation. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.conversations.list_evaluations( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/conversations/{jsonable_encoder(id)}/evaluations", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListEvaluationsResponse, - parse_obj_as( - type_=ListEvaluationsResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def list_memories( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> ListMemoriesResponse: - """ - List memories extracted from a specific conversation. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListMemoriesResponse - Memories written during this conversation. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.conversations.list_memories( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/conversations/{jsonable_encoder(id)}/memories", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListMemoriesResponse, - parse_obj_as( - type_=ListMemoriesResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/src/speechify/tts/errors/__init__.py b/src/speechify/tts/errors/__init__.py deleted file mode 100644 index 4bf158a..0000000 --- a/src/speechify/tts/errors/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .bad_request_error import BadRequestError -from .conflict_error import ConflictError -from .content_too_large_error import ContentTooLargeError -from .forbidden_error import ForbiddenError -from .internal_server_error import InternalServerError -from .not_found_error import NotFoundError -from .payment_required_error import PaymentRequiredError -from .unauthorized_error import UnauthorizedError - -__all__ = [ - "BadRequestError", - "ConflictError", - "ContentTooLargeError", - "ForbiddenError", - "InternalServerError", - "NotFoundError", - "PaymentRequiredError", - "UnauthorizedError", -] diff --git a/src/speechify/tts/errors/bad_request_error.py b/src/speechify/tts/errors/bad_request_error.py deleted file mode 100644 index 66ac1e3..0000000 --- a/src/speechify/tts/errors/bad_request_error.py +++ /dev/null @@ -1,9 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.api_error import ApiError -import typing - - -class BadRequestError(ApiError): - def __init__(self, body: typing.Optional[typing.Any]): - super().__init__(status_code=400, body=body) diff --git a/src/speechify/tts/errors/conflict_error.py b/src/speechify/tts/errors/conflict_error.py deleted file mode 100644 index d7a46cb..0000000 --- a/src/speechify/tts/errors/conflict_error.py +++ /dev/null @@ -1,9 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.api_error import ApiError -import typing - - -class ConflictError(ApiError): - def __init__(self, body: typing.Optional[typing.Any]): - super().__init__(status_code=409, body=body) diff --git a/src/speechify/tts/errors/content_too_large_error.py b/src/speechify/tts/errors/content_too_large_error.py deleted file mode 100644 index d418735..0000000 --- a/src/speechify/tts/errors/content_too_large_error.py +++ /dev/null @@ -1,9 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.api_error import ApiError -import typing - - -class ContentTooLargeError(ApiError): - def __init__(self, body: typing.Optional[typing.Any]): - super().__init__(status_code=413, body=body) diff --git a/src/speechify/tts/errors/forbidden_error.py b/src/speechify/tts/errors/forbidden_error.py deleted file mode 100644 index cc295c8..0000000 --- a/src/speechify/tts/errors/forbidden_error.py +++ /dev/null @@ -1,9 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.api_error import ApiError -import typing - - -class ForbiddenError(ApiError): - def __init__(self, body: typing.Optional[typing.Any]): - super().__init__(status_code=403, body=body) diff --git a/src/speechify/tts/errors/internal_server_error.py b/src/speechify/tts/errors/internal_server_error.py deleted file mode 100644 index 2bd9889..0000000 --- a/src/speechify/tts/errors/internal_server_error.py +++ /dev/null @@ -1,9 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.api_error import ApiError -import typing - - -class InternalServerError(ApiError): - def __init__(self, body: typing.Optional[typing.Any]): - super().__init__(status_code=500, body=body) diff --git a/src/speechify/tts/errors/not_found_error.py b/src/speechify/tts/errors/not_found_error.py deleted file mode 100644 index f5fb993..0000000 --- a/src/speechify/tts/errors/not_found_error.py +++ /dev/null @@ -1,9 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.api_error import ApiError -import typing - - -class NotFoundError(ApiError): - def __init__(self, body: typing.Optional[typing.Any]): - super().__init__(status_code=404, body=body) diff --git a/src/speechify/tts/errors/payment_required_error.py b/src/speechify/tts/errors/payment_required_error.py deleted file mode 100644 index 3a82ecc..0000000 --- a/src/speechify/tts/errors/payment_required_error.py +++ /dev/null @@ -1,9 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.api_error import ApiError -import typing - - -class PaymentRequiredError(ApiError): - def __init__(self, body: typing.Optional[typing.Any]): - super().__init__(status_code=402, body=body) diff --git a/src/speechify/tts/errors/unauthorized_error.py b/src/speechify/tts/errors/unauthorized_error.py deleted file mode 100644 index b8bb12b..0000000 --- a/src/speechify/tts/errors/unauthorized_error.py +++ /dev/null @@ -1,9 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.api_error import ApiError -import typing - - -class UnauthorizedError(ApiError): - def __init__(self, body: typing.Optional[typing.Any]): - super().__init__(status_code=401, body=body) diff --git a/src/speechify/tts/knowledge_bases/__init__.py b/src/speechify/tts/knowledge_bases/__init__.py deleted file mode 100644 index f3ea265..0000000 --- a/src/speechify/tts/knowledge_bases/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - diff --git a/src/speechify/tts/knowledge_bases/client.py b/src/speechify/tts/knowledge_bases/client.py deleted file mode 100644 index 534ae91..0000000 --- a/src/speechify/tts/knowledge_bases/client.py +++ /dev/null @@ -1,1722 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from ...core.client_wrapper import SyncClientWrapper -from ...core.request_options import RequestOptions -from ..types.list_knowledge_bases_response import ListKnowledgeBasesResponse -from ...core.pydantic_utilities import parse_obj_as -from ..errors.unauthorized_error import UnauthorizedError -from json.decoder import JSONDecodeError -from ...core.api_error import ApiError -from ..types.knowledge_base import KnowledgeBase -from ..errors.bad_request_error import BadRequestError -from ...core.jsonable_encoder import jsonable_encoder -from ..errors.not_found_error import NotFoundError -from ..types.list_knowledge_base_documents_response import ListKnowledgeBaseDocumentsResponse -from ... import core -from ..types.knowledge_base_document import KnowledgeBaseDocument -from ..errors.content_too_large_error import ContentTooLargeError -from ..types.list_knowledge_base_chunks_response import ListKnowledgeBaseChunksResponse -from ..types.search_knowledge_bases_response import SearchKnowledgeBasesResponse -from ...core.client_wrapper import AsyncClientWrapper - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class KnowledgeBasesClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> ListKnowledgeBasesResponse: - """ - List knowledge bases owned by the caller. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListKnowledgeBasesResponse - The knowledge bases for the caller. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.knowledge_bases.list() - """ - _response = self._client_wrapper.httpx_client.request( - "v1/knowledge-bases", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListKnowledgeBasesResponse, - parse_obj_as( - type_=ListKnowledgeBasesResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def create( - self, - *, - name: str, - description: typing.Optional[str] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> KnowledgeBase: - """ - Create a new knowledge base. - - Parameters - ---------- - name : str - Human-readable label. - - description : typing.Optional[str] - Optional description. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - KnowledgeBase - The created knowledge base. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.knowledge_bases.create( - name="name", - ) - """ - _response = self._client_wrapper.httpx_client.request( - "v1/knowledge-bases", - method="POST", - json={ - "name": name, - "description": description, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - KnowledgeBase, - parse_obj_as( - type_=KnowledgeBase, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def get(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> KnowledgeBase: - """ - Retrieve a knowledge base by ID. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - KnowledgeBase - The requested knowledge base. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.knowledge_bases.get( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/knowledge-bases/{jsonable_encoder(id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - KnowledgeBase, - parse_obj_as( - type_=KnowledgeBase, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def delete(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: - """ - Soft-delete a knowledge base. Documents and chunks are cascaded. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.knowledge_bases.delete( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/knowledge-bases/{jsonable_encoder(id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def update( - self, - id: str, - *, - name: typing.Optional[str] = OMIT, - description: typing.Optional[str] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> KnowledgeBase: - """ - Update a knowledge base. - - Parameters - ---------- - id : str - - name : typing.Optional[str] - - description : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - KnowledgeBase - The updated knowledge base. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.knowledge_bases.update( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/knowledge-bases/{jsonable_encoder(id)}", - method="PATCH", - json={ - "name": name, - "description": description, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - KnowledgeBase, - parse_obj_as( - type_=KnowledgeBase, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def list_documents( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> ListKnowledgeBaseDocumentsResponse: - """ - List documents ingested into a knowledge base. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListKnowledgeBaseDocumentsResponse - The documents in the knowledge base. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.knowledge_bases.list_documents( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/knowledge-bases/{jsonable_encoder(id)}/documents", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListKnowledgeBaseDocumentsResponse, - parse_obj_as( - type_=ListKnowledgeBaseDocumentsResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def upload_document( - self, id: str, *, file: core.File, request_options: typing.Optional[RequestOptions] = None - ) -> KnowledgeBaseDocument: - """ - Upload a document (PDF, plain text, markdown, or HTML) to a - knowledge base. The document is extracted, chunked, embedded, and - indexed synchronously; expect a few seconds per MB of input. - Maximum 10 MB per upload. - - Parameters - ---------- - id : str - - file : core.File - See core.File for more documentation - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - KnowledgeBaseDocument - The ingested document record. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.knowledge_bases.upload_document( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/knowledge-bases/{jsonable_encoder(id)}/documents", - method="POST", - data={}, - files={ - "file": file, - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - KnowledgeBaseDocument, - parse_obj_as( - type_=KnowledgeBaseDocument, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 413: - raise ContentTooLargeError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def get_document( - self, doc_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> KnowledgeBaseDocument: - """ - Retrieve a document by ID. - - Parameters - ---------- - doc_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - KnowledgeBaseDocument - The document record. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.knowledge_bases.get_document( - doc_id="docId", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/knowledge-bases/documents/{jsonable_encoder(doc_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - KnowledgeBaseDocument, - parse_obj_as( - type_=KnowledgeBaseDocument, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def delete_document(self, doc_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: - """ - Delete a document and all its chunks. - - Parameters - ---------- - doc_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.knowledge_bases.delete_document( - doc_id="docId", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/knowledge-bases/documents/{jsonable_encoder(doc_id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def list_chunks( - self, doc_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> ListKnowledgeBaseChunksResponse: - """ - List the chunks for a document. - - Parameters - ---------- - doc_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListKnowledgeBaseChunksResponse - The chunks for the document. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.knowledge_bases.list_chunks( - doc_id="docId", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/knowledge-bases/documents/{jsonable_encoder(doc_id)}/chunks", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListKnowledgeBaseChunksResponse, - parse_obj_as( - type_=ListKnowledgeBaseChunksResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def search( - self, - *, - query: str, - kb_ids: typing.Sequence[str], - top_k: typing.Optional[int] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> SearchKnowledgeBasesResponse: - """ - Semantic search across a caller-owned list of knowledge bases. - Returns ranked chunks with source filename and a cosine-similarity - score. Limited to 50 results per request. - - Parameters - ---------- - query : str - Natural-language search query. - - kb_ids : typing.Sequence[str] - Knowledge bases to search across. Results scoped to caller-owned entries; unknown IDs are silently ignored. - - top_k : typing.Optional[int] - Max hits to return (default 5, capped at 50). - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - SearchKnowledgeBasesResponse - Ranked search hits across the selected knowledge bases. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.knowledge_bases.search( - query="query", - kb_ids=["kb_ids"], - ) - """ - _response = self._client_wrapper.httpx_client.request( - "v1/knowledge-bases/search", - method="POST", - json={ - "query": query, - "kb_ids": kb_ids, - "top_k": top_k, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - SearchKnowledgeBasesResponse, - parse_obj_as( - type_=SearchKnowledgeBasesResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - -class AsyncKnowledgeBasesClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> ListKnowledgeBasesResponse: - """ - List knowledge bases owned by the caller. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListKnowledgeBasesResponse - The knowledge bases for the caller. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.knowledge_bases.list() - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/knowledge-bases", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListKnowledgeBasesResponse, - parse_obj_as( - type_=ListKnowledgeBasesResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def create( - self, - *, - name: str, - description: typing.Optional[str] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> KnowledgeBase: - """ - Create a new knowledge base. - - Parameters - ---------- - name : str - Human-readable label. - - description : typing.Optional[str] - Optional description. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - KnowledgeBase - The created knowledge base. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.knowledge_bases.create( - name="name", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/knowledge-bases", - method="POST", - json={ - "name": name, - "description": description, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - KnowledgeBase, - parse_obj_as( - type_=KnowledgeBase, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def get(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> KnowledgeBase: - """ - Retrieve a knowledge base by ID. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - KnowledgeBase - The requested knowledge base. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.knowledge_bases.get( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/knowledge-bases/{jsonable_encoder(id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - KnowledgeBase, - parse_obj_as( - type_=KnowledgeBase, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def delete(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: - """ - Soft-delete a knowledge base. Documents and chunks are cascaded. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.knowledge_bases.delete( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/knowledge-bases/{jsonable_encoder(id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def update( - self, - id: str, - *, - name: typing.Optional[str] = OMIT, - description: typing.Optional[str] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> KnowledgeBase: - """ - Update a knowledge base. - - Parameters - ---------- - id : str - - name : typing.Optional[str] - - description : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - KnowledgeBase - The updated knowledge base. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.knowledge_bases.update( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/knowledge-bases/{jsonable_encoder(id)}", - method="PATCH", - json={ - "name": name, - "description": description, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - KnowledgeBase, - parse_obj_as( - type_=KnowledgeBase, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def list_documents( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> ListKnowledgeBaseDocumentsResponse: - """ - List documents ingested into a knowledge base. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListKnowledgeBaseDocumentsResponse - The documents in the knowledge base. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.knowledge_bases.list_documents( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/knowledge-bases/{jsonable_encoder(id)}/documents", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListKnowledgeBaseDocumentsResponse, - parse_obj_as( - type_=ListKnowledgeBaseDocumentsResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def upload_document( - self, id: str, *, file: core.File, request_options: typing.Optional[RequestOptions] = None - ) -> KnowledgeBaseDocument: - """ - Upload a document (PDF, plain text, markdown, or HTML) to a - knowledge base. The document is extracted, chunked, embedded, and - indexed synchronously; expect a few seconds per MB of input. - Maximum 10 MB per upload. - - Parameters - ---------- - id : str - - file : core.File - See core.File for more documentation - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - KnowledgeBaseDocument - The ingested document record. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.knowledge_bases.upload_document( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/knowledge-bases/{jsonable_encoder(id)}/documents", - method="POST", - data={}, - files={ - "file": file, - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - KnowledgeBaseDocument, - parse_obj_as( - type_=KnowledgeBaseDocument, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 413: - raise ContentTooLargeError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def get_document( - self, doc_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> KnowledgeBaseDocument: - """ - Retrieve a document by ID. - - Parameters - ---------- - doc_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - KnowledgeBaseDocument - The document record. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.knowledge_bases.get_document( - doc_id="docId", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/knowledge-bases/documents/{jsonable_encoder(doc_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - KnowledgeBaseDocument, - parse_obj_as( - type_=KnowledgeBaseDocument, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def delete_document(self, doc_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: - """ - Delete a document and all its chunks. - - Parameters - ---------- - doc_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.knowledge_bases.delete_document( - doc_id="docId", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/knowledge-bases/documents/{jsonable_encoder(doc_id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def list_chunks( - self, doc_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> ListKnowledgeBaseChunksResponse: - """ - List the chunks for a document. - - Parameters - ---------- - doc_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListKnowledgeBaseChunksResponse - The chunks for the document. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.knowledge_bases.list_chunks( - doc_id="docId", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/knowledge-bases/documents/{jsonable_encoder(doc_id)}/chunks", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListKnowledgeBaseChunksResponse, - parse_obj_as( - type_=ListKnowledgeBaseChunksResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def search( - self, - *, - query: str, - kb_ids: typing.Sequence[str], - top_k: typing.Optional[int] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> SearchKnowledgeBasesResponse: - """ - Semantic search across a caller-owned list of knowledge bases. - Returns ranked chunks with source filename and a cosine-similarity - score. Limited to 50 results per request. - - Parameters - ---------- - query : str - Natural-language search query. - - kb_ids : typing.Sequence[str] - Knowledge bases to search across. Results scoped to caller-owned entries; unknown IDs are silently ignored. - - top_k : typing.Optional[int] - Max hits to return (default 5, capped at 50). - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - SearchKnowledgeBasesResponse - Ranked search hits across the selected knowledge bases. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.knowledge_bases.search( - query="query", - kb_ids=["kb_ids"], - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/knowledge-bases/search", - method="POST", - json={ - "query": query, - "kb_ids": kb_ids, - "top_k": top_k, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - SearchKnowledgeBasesResponse, - parse_obj_as( - type_=SearchKnowledgeBasesResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/src/speechify/tts/memories/__init__.py b/src/speechify/tts/memories/__init__.py deleted file mode 100644 index f3ea265..0000000 --- a/src/speechify/tts/memories/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - diff --git a/src/speechify/tts/memories/client.py b/src/speechify/tts/memories/client.py deleted file mode 100644 index afb9f84..0000000 --- a/src/speechify/tts/memories/client.py +++ /dev/null @@ -1,148 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.client_wrapper import SyncClientWrapper -import typing -from ...core.request_options import RequestOptions -from ...core.jsonable_encoder import jsonable_encoder -from ..errors.unauthorized_error import UnauthorizedError -from ...core.pydantic_utilities import parse_obj_as -from ..errors.not_found_error import NotFoundError -from json.decoder import JSONDecodeError -from ...core.api_error import ApiError -from ...core.client_wrapper import AsyncClientWrapper - - -class MemoriesClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def delete(self, memory_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: - """ - Soft-delete one memory row. - - Parameters - ---------- - memory_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.memories.delete( - memory_id="memoryId", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/memories/{jsonable_encoder(memory_id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - -class AsyncMemoriesClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def delete(self, memory_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: - """ - Soft-delete one memory row. - - Parameters - ---------- - memory_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.memories.delete( - memory_id="memoryId", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/memories/{jsonable_encoder(memory_id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/src/speechify/tts/outbound_calls/client.py b/src/speechify/tts/outbound_calls/client.py deleted file mode 100644 index d8f9e3b..0000000 --- a/src/speechify/tts/outbound_calls/client.py +++ /dev/null @@ -1,213 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from ...core.client_wrapper import SyncClientWrapper -from ...core.request_options import RequestOptions -from ...core.pydantic_utilities import parse_obj_as -from ..errors.bad_request_error import BadRequestError -from ..errors.unauthorized_error import UnauthorizedError -from ..errors.not_found_error import NotFoundError -from json.decoder import JSONDecodeError -from ...core.api_error import ApiError -from ...core.client_wrapper import AsyncClientWrapper - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class OutboundCallsClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def create( - self, *, request: typing.Optional[typing.Any] = None, request_options: typing.Optional[RequestOptions] = None - ) -> typing.Optional[typing.Any]: - """ - Place an outbound call from an agent to a phone number. LiveKit - originates the SIP INVITE through the outbound trunk bound to the - agent's workspace; the agent worker is dispatched into the room - automatically. - - The response is returned as soon as LiveKit accepts the INVITE. - Poll `GET /v1/conversations/{conversation_id}` for status - transitions: `pending` → `active` (answered) → `completed`. - - Requires a Twilio or BYOC trunk. LiveKit-native numbers are - inbound-only. - - Parameters - ---------- - request : typing.Optional[typing.Any] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - typing.Optional[typing.Any] - The outbound call was accepted by LiveKit. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.outbound_calls.create( - request={"key": "value"}, - ) - """ - _response = self._client_wrapper.httpx_client.request( - "v1/outbound-calls", - method="POST", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - -class AsyncOutboundCallsClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def create( - self, *, request: typing.Optional[typing.Any] = None, request_options: typing.Optional[RequestOptions] = None - ) -> typing.Optional[typing.Any]: - """ - Place an outbound call from an agent to a phone number. LiveKit - originates the SIP INVITE through the outbound trunk bound to the - agent's workspace; the agent worker is dispatched into the room - automatically. - - The response is returned as soon as LiveKit accepts the INVITE. - Poll `GET /v1/conversations/{conversation_id}` for status - transitions: `pending` → `active` (answered) → `completed`. - - Requires a Twilio or BYOC trunk. LiveKit-native numbers are - inbound-only. - - Parameters - ---------- - request : typing.Optional[typing.Any] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - typing.Optional[typing.Any] - The outbound call was accepted by LiveKit. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.outbound_calls.create( - request={"key": "value"}, - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/outbound-calls", - method="POST", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/src/speechify/tts/phone_numbers/__init__.py b/src/speechify/tts/phone_numbers/__init__.py deleted file mode 100644 index f3ea265..0000000 --- a/src/speechify/tts/phone_numbers/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - diff --git a/src/speechify/tts/phone_numbers/client.py b/src/speechify/tts/phone_numbers/client.py deleted file mode 100644 index 956091a..0000000 --- a/src/speechify/tts/phone_numbers/client.py +++ /dev/null @@ -1,787 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from ...core.client_wrapper import SyncClientWrapper -from ...core.request_options import RequestOptions -from ...core.pydantic_utilities import parse_obj_as -from ..errors.unauthorized_error import UnauthorizedError -from json.decoder import JSONDecodeError -from ...core.api_error import ApiError -from ..errors.bad_request_error import BadRequestError -from ..errors.payment_required_error import PaymentRequiredError -from ...core.jsonable_encoder import jsonable_encoder -from ..errors.not_found_error import NotFoundError -from ...core.client_wrapper import AsyncClientWrapper - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class PhoneNumbersClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> typing.Optional[typing.Any]: - """ - List all phone numbers in the caller's workspace. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - typing.Optional[typing.Any] - The phone numbers for the workspace. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.phone_numbers.list() - """ - _response = self._client_wrapper.httpx_client.request( - "v1/phone-numbers", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def import_( - self, *, request: typing.Optional[typing.Any] = None, request_options: typing.Optional[RequestOptions] = None - ) -> typing.Optional[typing.Any]: - """ - Import a phone number into the workspace. The `source` field - determines the provisioning path: - - - `livekit` - LiveKit purchases the number on your behalf. US - inbound only. Quickest path for local testing. - - `twilio` - Provide your Twilio Account SID, Auth Token, and - the E.164 number you already own. We provision an Elastic SIP - Trunk on your Twilio account automatically. - - `byoc` - Provide an existing SIP trunk ID. The number is - registered against that trunk. - - Returns 402 when the workspace has reached the 100-number cap. - - Parameters - ---------- - request : typing.Optional[typing.Any] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - typing.Optional[typing.Any] - The imported phone number. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.phone_numbers.import_( - request={"key": "value"}, - ) - """ - _response = self._client_wrapper.httpx_client.request( - "v1/phone-numbers", - method="POST", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 402: - raise PaymentRequiredError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def get(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> typing.Optional[typing.Any]: - """ - Retrieve a phone number by ID. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - typing.Optional[typing.Any] - The requested phone number. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.phone_numbers.get( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/phone-numbers/{jsonable_encoder(id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def delete(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: - """ - Delete a phone number from the workspace. For Twilio and LiveKit - numbers this also deprovisions the backing SIP trunk and dispatch - rule on LiveKit Cloud. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.phone_numbers.delete( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/phone-numbers/{jsonable_encoder(id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def update( - self, - id: str, - *, - request: typing.Optional[typing.Any] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> typing.Optional[typing.Any]: - """ - Update a phone number. Only `label` and `agent_id` are mutable; - `source` and `e164` are immutable after import. Pass `null` for - `agent_id` to unbind the number from its current agent. - - Parameters - ---------- - id : str - - request : typing.Optional[typing.Any] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - typing.Optional[typing.Any] - The updated phone number. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.phone_numbers.update( - id="id", - request={"key": "value"}, - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/phone-numbers/{jsonable_encoder(id)}", - method="PATCH", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - -class AsyncPhoneNumbersClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> typing.Optional[typing.Any]: - """ - List all phone numbers in the caller's workspace. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - typing.Optional[typing.Any] - The phone numbers for the workspace. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.phone_numbers.list() - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/phone-numbers", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def import_( - self, *, request: typing.Optional[typing.Any] = None, request_options: typing.Optional[RequestOptions] = None - ) -> typing.Optional[typing.Any]: - """ - Import a phone number into the workspace. The `source` field - determines the provisioning path: - - - `livekit` - LiveKit purchases the number on your behalf. US - inbound only. Quickest path for local testing. - - `twilio` - Provide your Twilio Account SID, Auth Token, and - the E.164 number you already own. We provision an Elastic SIP - Trunk on your Twilio account automatically. - - `byoc` - Provide an existing SIP trunk ID. The number is - registered against that trunk. - - Returns 402 when the workspace has reached the 100-number cap. - - Parameters - ---------- - request : typing.Optional[typing.Any] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - typing.Optional[typing.Any] - The imported phone number. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.phone_numbers.import_( - request={"key": "value"}, - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/phone-numbers", - method="POST", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 402: - raise PaymentRequiredError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def get( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> typing.Optional[typing.Any]: - """ - Retrieve a phone number by ID. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - typing.Optional[typing.Any] - The requested phone number. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.phone_numbers.get( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/phone-numbers/{jsonable_encoder(id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def delete(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: - """ - Delete a phone number from the workspace. For Twilio and LiveKit - numbers this also deprovisions the backing SIP trunk and dispatch - rule on LiveKit Cloud. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.phone_numbers.delete( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/phone-numbers/{jsonable_encoder(id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def update( - self, - id: str, - *, - request: typing.Optional[typing.Any] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> typing.Optional[typing.Any]: - """ - Update a phone number. Only `label` and `agent_id` are mutable; - `source` and `e164` are immutable after import. Pass `null` for - `agent_id` to unbind the number from its current agent. - - Parameters - ---------- - id : str - - request : typing.Optional[typing.Any] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - typing.Optional[typing.Any] - The updated phone number. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.phone_numbers.update( - id="id", - request={"key": "value"}, - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/phone-numbers/{jsonable_encoder(id)}", - method="PATCH", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/src/speechify/tts/sip_trunks/__init__.py b/src/speechify/tts/sip_trunks/__init__.py deleted file mode 100644 index f3ea265..0000000 --- a/src/speechify/tts/sip_trunks/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - diff --git a/src/speechify/tts/sip_trunks/client.py b/src/speechify/tts/sip_trunks/client.py deleted file mode 100644 index 2d837a8..0000000 --- a/src/speechify/tts/sip_trunks/client.py +++ /dev/null @@ -1,589 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from ...core.client_wrapper import SyncClientWrapper -from ...core.request_options import RequestOptions -from ...core.pydantic_utilities import parse_obj_as -from ..errors.unauthorized_error import UnauthorizedError -from json.decoder import JSONDecodeError -from ...core.api_error import ApiError -from ..errors.bad_request_error import BadRequestError -from ..errors.payment_required_error import PaymentRequiredError -from ...core.jsonable_encoder import jsonable_encoder -from ..errors.not_found_error import NotFoundError -from ...core.client_wrapper import AsyncClientWrapper - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class SipTrunksClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> typing.Optional[typing.Any]: - """ - List all SIP trunks in the caller's workspace. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - typing.Optional[typing.Any] - The SIP trunks for the workspace. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.sip_trunks.list() - """ - _response = self._client_wrapper.httpx_client.request( - "v1/sip-trunks", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def create( - self, *, request: typing.Optional[typing.Any] = None, request_options: typing.Optional[RequestOptions] = None - ) -> typing.Optional[typing.Any]: - """ - Create a SIP trunk. For `kind=byoc` supply `sip_address` plus - optional digest credentials and IP allowlist. For `kind=twilio` - use `ImportPhoneNumber` with a `twilio` spec instead - trunk - creation is handled automatically. Returns 402 when the workspace - has reached the 20-trunk cap. - - Parameters - ---------- - request : typing.Optional[typing.Any] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - typing.Optional[typing.Any] - The created SIP trunk. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.sip_trunks.create( - request={"key": "value"}, - ) - """ - _response = self._client_wrapper.httpx_client.request( - "v1/sip-trunks", - method="POST", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 402: - raise PaymentRequiredError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def get(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> typing.Optional[typing.Any]: - """ - Retrieve a SIP trunk by ID. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - typing.Optional[typing.Any] - The requested SIP trunk. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.sip_trunks.get( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/sip-trunks/{jsonable_encoder(id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def delete(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: - """ - Delete a SIP trunk. This also removes the backing LiveKit inbound - trunk, outbound trunk, and dispatch rule if they were provisioned - by us. Phone numbers attached to this trunk are left in place but - become non-functional until rebound to a new trunk. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.sip_trunks.delete( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/sip-trunks/{jsonable_encoder(id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - -class AsyncSipTrunksClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> typing.Optional[typing.Any]: - """ - List all SIP trunks in the caller's workspace. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - typing.Optional[typing.Any] - The SIP trunks for the workspace. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.sip_trunks.list() - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/sip-trunks", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def create( - self, *, request: typing.Optional[typing.Any] = None, request_options: typing.Optional[RequestOptions] = None - ) -> typing.Optional[typing.Any]: - """ - Create a SIP trunk. For `kind=byoc` supply `sip_address` plus - optional digest credentials and IP allowlist. For `kind=twilio` - use `ImportPhoneNumber` with a `twilio` spec instead - trunk - creation is handled automatically. Returns 402 when the workspace - has reached the 20-trunk cap. - - Parameters - ---------- - request : typing.Optional[typing.Any] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - typing.Optional[typing.Any] - The created SIP trunk. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.sip_trunks.create( - request={"key": "value"}, - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/sip-trunks", - method="POST", - json=request, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 402: - raise PaymentRequiredError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def get( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> typing.Optional[typing.Any]: - """ - Retrieve a SIP trunk by ID. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - typing.Optional[typing.Any] - The requested SIP trunk. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.sip_trunks.get( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/sip-trunks/{jsonable_encoder(id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def delete(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: - """ - Delete a SIP trunk. This also removes the backing LiveKit inbound - trunk, outbound trunk, and dispatch rule if they were provisioned - by us. Phone numbers attached to this trunk are left in place but - become non-functional until rebound to a new trunk. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.sip_trunks.delete( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/sip-trunks/{jsonable_encoder(id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/src/speechify/tts/tools/__init__.py b/src/speechify/tts/tools/__init__.py deleted file mode 100644 index 3d29f5d..0000000 --- a/src/speechify/tts/tools/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .types import CreateToolRequestConfig, UpdateToolRequestConfig - -__all__ = ["CreateToolRequestConfig", "UpdateToolRequestConfig"] diff --git a/src/speechify/tts/tools/client.py b/src/speechify/tts/tools/client.py deleted file mode 100644 index 56cbb77..0000000 --- a/src/speechify/tts/tools/client.py +++ /dev/null @@ -1,828 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from ...core.client_wrapper import SyncClientWrapper -from ...core.request_options import RequestOptions -from ..types.list_tools_response import ListToolsResponse -from ...core.pydantic_utilities import parse_obj_as -from ..errors.unauthorized_error import UnauthorizedError -from json.decoder import JSONDecodeError -from ...core.api_error import ApiError -from ..types.tool_kind import ToolKind -from .types.create_tool_request_config import CreateToolRequestConfig -from ..types.tool import Tool -from ...core.serialization import convert_and_respect_annotation_metadata -from ..errors.bad_request_error import BadRequestError -from ...core.jsonable_encoder import jsonable_encoder -from ..errors.not_found_error import NotFoundError -from .types.update_tool_request_config import UpdateToolRequestConfig -from ...core.client_wrapper import AsyncClientWrapper - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class ToolsClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> ListToolsResponse: - """ - List tools owned by the caller. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListToolsResponse - A list of tools. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.tools.list() - """ - _response = self._client_wrapper.httpx_client.request( - "v1/tools", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListToolsResponse, - parse_obj_as( - type_=ListToolsResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def create( - self, - *, - name: str, - description: str, - kind: ToolKind, - config: CreateToolRequestConfig, - request_options: typing.Optional[RequestOptions] = None, - ) -> Tool: - """ - Create a tool. For webhook tools, the response includes the HMAC - `webhook_secret` exactly once — store it immediately; subsequent - reads return a masked placeholder. - - Parameters - ---------- - name : str - - description : str - - kind : ToolKind - - config : CreateToolRequestConfig - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Tool - The created tool. - - Examples - -------- - from speechify import Speechify - from speechify.tts import SystemToolConfig - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.tools.create( - name="name", - description="description", - kind="system", - config=SystemToolConfig( - builtin="end_call", - ), - ) - """ - _response = self._client_wrapper.httpx_client.request( - "v1/tools", - method="POST", - json={ - "name": name, - "description": description, - "kind": kind, - "config": convert_and_respect_annotation_metadata( - object_=config, annotation=CreateToolRequestConfig, direction="write" - ), - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - Tool, - parse_obj_as( - type_=Tool, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def get(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> Tool: - """ - Retrieve a tool by ID. Webhook secrets are always masked here. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Tool - The requested tool. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.tools.get( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/tools/{jsonable_encoder(id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - Tool, - parse_obj_as( - type_=Tool, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def delete(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: - """ - Delete a tool. Agents that had it attached get a soft-detach. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.tools.delete( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/tools/{jsonable_encoder(id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def update( - self, - id: str, - *, - name: typing.Optional[str] = OMIT, - description: typing.Optional[str] = OMIT, - config: typing.Optional[UpdateToolRequestConfig] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> Tool: - """ - Update a tool. Tool kind is immutable — create a new tool to change it. - - Parameters - ---------- - id : str - - name : typing.Optional[str] - - description : typing.Optional[str] - - config : typing.Optional[UpdateToolRequestConfig] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Tool - The updated tool. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.tools.update( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/tools/{jsonable_encoder(id)}", - method="PATCH", - json={ - "name": name, - "description": description, - "config": convert_and_respect_annotation_metadata( - object_=config, annotation=UpdateToolRequestConfig, direction="write" - ), - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - Tool, - parse_obj_as( - type_=Tool, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - -class AsyncToolsClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> ListToolsResponse: - """ - List tools owned by the caller. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListToolsResponse - A list of tools. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.tools.list() - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/tools", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - ListToolsResponse, - parse_obj_as( - type_=ListToolsResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def create( - self, - *, - name: str, - description: str, - kind: ToolKind, - config: CreateToolRequestConfig, - request_options: typing.Optional[RequestOptions] = None, - ) -> Tool: - """ - Create a tool. For webhook tools, the response includes the HMAC - `webhook_secret` exactly once — store it immediately; subsequent - reads return a masked placeholder. - - Parameters - ---------- - name : str - - description : str - - kind : ToolKind - - config : CreateToolRequestConfig - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Tool - The created tool. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - from speechify.tts import SystemToolConfig - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.tools.create( - name="name", - description="description", - kind="system", - config=SystemToolConfig( - builtin="end_call", - ), - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/tools", - method="POST", - json={ - "name": name, - "description": description, - "kind": kind, - "config": convert_and_respect_annotation_metadata( - object_=config, annotation=CreateToolRequestConfig, direction="write" - ), - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - Tool, - parse_obj_as( - type_=Tool, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def get(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> Tool: - """ - Retrieve a tool by ID. Webhook secrets are always masked here. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Tool - The requested tool. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.tools.get( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/tools/{jsonable_encoder(id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - Tool, - parse_obj_as( - type_=Tool, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def delete(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: - """ - Delete a tool. Agents that had it attached get a soft-detach. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.tools.delete( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/tools/{jsonable_encoder(id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def update( - self, - id: str, - *, - name: typing.Optional[str] = OMIT, - description: typing.Optional[str] = OMIT, - config: typing.Optional[UpdateToolRequestConfig] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> Tool: - """ - Update a tool. Tool kind is immutable — create a new tool to change it. - - Parameters - ---------- - id : str - - name : typing.Optional[str] - - description : typing.Optional[str] - - config : typing.Optional[UpdateToolRequestConfig] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Tool - The updated tool. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.tools.update( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/tools/{jsonable_encoder(id)}", - method="PATCH", - json={ - "name": name, - "description": description, - "config": convert_and_respect_annotation_metadata( - object_=config, annotation=UpdateToolRequestConfig, direction="write" - ), - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - Tool, - parse_obj_as( - type_=Tool, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/src/speechify/tts/tools/types/__init__.py b/src/speechify/tts/tools/types/__init__.py deleted file mode 100644 index 5bc7f9e..0000000 --- a/src/speechify/tts/tools/types/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .create_tool_request_config import CreateToolRequestConfig -from .update_tool_request_config import UpdateToolRequestConfig - -__all__ = ["CreateToolRequestConfig", "UpdateToolRequestConfig"] diff --git a/src/speechify/tts/tools/types/create_tool_request_config.py b/src/speechify/tts/tools/types/create_tool_request_config.py deleted file mode 100644 index d1a85f1..0000000 --- a/src/speechify/tts/tools/types/create_tool_request_config.py +++ /dev/null @@ -1,8 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from ...types.system_tool_config import SystemToolConfig -from ...types.webhook_tool_config import WebhookToolConfig -from ...types.client_tool_config import ClientToolConfig - -CreateToolRequestConfig = typing.Union[SystemToolConfig, WebhookToolConfig, ClientToolConfig] diff --git a/src/speechify/tts/tools/types/update_tool_request_config.py b/src/speechify/tts/tools/types/update_tool_request_config.py deleted file mode 100644 index b20f9b7..0000000 --- a/src/speechify/tts/tools/types/update_tool_request_config.py +++ /dev/null @@ -1,8 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from ...types.system_tool_config import SystemToolConfig -from ...types.webhook_tool_config import WebhookToolConfig -from ...types.client_tool_config import ClientToolConfig - -UpdateToolRequestConfig = typing.Union[SystemToolConfig, WebhookToolConfig, ClientToolConfig] diff --git a/src/speechify/tts/types/__init__.py b/src/speechify/tts/types/__init__.py deleted file mode 100644 index accbecf..0000000 --- a/src/speechify/tts/types/__init__.py +++ /dev/null @@ -1,235 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .access_token import AccessToken -from .access_token_scope import AccessTokenScope -from .access_token_token_type import AccessTokenTokenType -from .agent import Agent -from .agent_test import AgentTest -from .agent_test_attachment import AgentTestAttachment -from .agent_test_config import AgentTestConfig -from .agent_test_folder import AgentTestFolder -from .agent_test_run import AgentTestRun -from .agent_test_with_last_run import AgentTestWithLastRun -from .api_key import ApiKey -from .batch_run_entry import BatchRunEntry -from .client_tool_config import ClientToolConfig -from .conversation import Conversation -from .conversation_stats import ConversationStats -from .conversation_status import ConversationStatus -from .conversation_transport import ConversationTransport -from .create_conversation_response import CreateConversationResponse -from .create_voice_language import CreateVoiceLanguage -from .create_voice_model import CreateVoiceModel -from .create_voice_model_name import CreateVoiceModelName -from .created_voice import CreatedVoice -from .created_voice_gender import CreatedVoiceGender -from .created_voice_type import CreatedVoiceType -from .data_collection_field import DataCollectionField -from .data_collection_field_type import DataCollectionFieldType -from .delete_memories_by_caller_response import DeleteMemoriesByCallerResponse -from .dynamic_variable import DynamicVariable -from .dynamic_variable_type import DynamicVariableType -from .evaluation import Evaluation -from .evaluation_config import EvaluationConfig -from .evaluation_criterion import EvaluationCriterion -from .evaluation_kind import EvaluationKind -from .evaluation_status import EvaluationStatus -from .get_speech_options_request import GetSpeechOptionsRequest -from .get_speech_response import GetSpeechResponse -from .get_speech_response_audio_format import GetSpeechResponseAudioFormat -from .get_stream_options_request import GetStreamOptionsRequest -from .get_voice import GetVoice -from .get_voice_gender import GetVoiceGender -from .get_voice_language import GetVoiceLanguage -from .get_voice_type import GetVoiceType -from .get_voices_model import GetVoicesModel -from .get_voices_model_name import GetVoicesModelName -from .invite import Invite -from .invite_preview import InvitePreview -from .invites_list_response import InvitesListResponse -from .knowledge_base import KnowledgeBase -from .knowledge_base_chunk import KnowledgeBaseChunk -from .knowledge_base_document import KnowledgeBaseDocument -from .knowledge_base_document_status import KnowledgeBaseDocumentStatus -from .knowledge_base_search_hit import KnowledgeBaseSearchHit -from .list_agent_test_attachments_response import ListAgentTestAttachmentsResponse -from .list_agent_test_folders_response import ListAgentTestFoldersResponse -from .list_agent_test_runs_response import ListAgentTestRunsResponse -from .list_agent_tests_response import ListAgentTestsResponse -from .list_agents_response import ListAgentsResponse -from .list_conversations_response import ListConversationsResponse -from .list_dynamic_variables_response import ListDynamicVariablesResponse -from .list_evaluations_response import ListEvaluationsResponse -from .list_knowledge_base_chunks_response import ListKnowledgeBaseChunksResponse -from .list_knowledge_base_documents_response import ListKnowledgeBaseDocumentsResponse -from .list_knowledge_bases_response import ListKnowledgeBasesResponse -from .list_memories_response import ListMemoriesResponse -from .list_messages_response import ListMessagesResponse -from .list_tests_response import ListTestsResponse -from .list_tools_response import ListToolsResponse -from .member import Member -from .member_role import MemberRole -from .members_list_response import MembersListResponse -from .memory import Memory -from .message import Message -from .message_role import MessageRole -from .mocking_strategy import MockingStrategy -from .nested_chunk import NestedChunk -from .no_match_behavior import NoMatchBehavior -from .o_auth_error import OAuthError -from .o_auth_error_error import OAuthErrorError -from .parameter_check import ParameterCheck -from .parameter_check_mode import ParameterCheckMode -from .parameter_check_result import ParameterCheckResult -from .run_agent_tests_response import RunAgentTestsResponse -from .run_batch_response import RunBatchResponse -from .scenario_config import ScenarioConfig -from .scenario_result import ScenarioResult -from .search_knowledge_bases_response import SearchKnowledgeBasesResponse -from .simulation_config import SimulationConfig -from .simulation_message import SimulationMessage -from .simulation_message_role import SimulationMessageRole -from .simulation_result import SimulationResult -from .simulation_tool_call import SimulationToolCall -from .speech_marks import SpeechMarks -from .system_tool_config import SystemToolConfig -from .system_tool_config_builtin import SystemToolConfigBuiltin -from .system_variable_doc import SystemVariableDoc -from .tenant import Tenant -from .tenant_data_region import TenantDataRegion -from .tenant_plan import TenantPlan -from .tenants_list_response import TenantsListResponse -from .test_run_result import TestRunResult -from .test_run_status import TestRunStatus -from .test_stats import TestStats -from .test_stats_bucket import TestStatsBucket -from .test_type import TestType -from .tool import Tool -from .tool_call_config import ToolCallConfig -from .tool_call_result import ToolCallResult -from .tool_config import ToolConfig -from .tool_kind import ToolKind -from .tool_mock import ToolMock -from .tool_mock_config import ToolMockConfig -from .tool_param import ToolParam -from .tool_param_type import ToolParamType -from .webhook_tool_config import WebhookToolConfig -from .webhook_tool_config_method import WebhookToolConfigMethod - -__all__ = [ - "AccessToken", - "AccessTokenScope", - "AccessTokenTokenType", - "Agent", - "AgentTest", - "AgentTestAttachment", - "AgentTestConfig", - "AgentTestFolder", - "AgentTestRun", - "AgentTestWithLastRun", - "ApiKey", - "BatchRunEntry", - "ClientToolConfig", - "Conversation", - "ConversationStats", - "ConversationStatus", - "ConversationTransport", - "CreateConversationResponse", - "CreateVoiceLanguage", - "CreateVoiceModel", - "CreateVoiceModelName", - "CreatedVoice", - "CreatedVoiceGender", - "CreatedVoiceType", - "DataCollectionField", - "DataCollectionFieldType", - "DeleteMemoriesByCallerResponse", - "DynamicVariable", - "DynamicVariableType", - "Evaluation", - "EvaluationConfig", - "EvaluationCriterion", - "EvaluationKind", - "EvaluationStatus", - "GetSpeechOptionsRequest", - "GetSpeechResponse", - "GetSpeechResponseAudioFormat", - "GetStreamOptionsRequest", - "GetVoice", - "GetVoiceGender", - "GetVoiceLanguage", - "GetVoiceType", - "GetVoicesModel", - "GetVoicesModelName", - "Invite", - "InvitePreview", - "InvitesListResponse", - "KnowledgeBase", - "KnowledgeBaseChunk", - "KnowledgeBaseDocument", - "KnowledgeBaseDocumentStatus", - "KnowledgeBaseSearchHit", - "ListAgentTestAttachmentsResponse", - "ListAgentTestFoldersResponse", - "ListAgentTestRunsResponse", - "ListAgentTestsResponse", - "ListAgentsResponse", - "ListConversationsResponse", - "ListDynamicVariablesResponse", - "ListEvaluationsResponse", - "ListKnowledgeBaseChunksResponse", - "ListKnowledgeBaseDocumentsResponse", - "ListKnowledgeBasesResponse", - "ListMemoriesResponse", - "ListMessagesResponse", - "ListTestsResponse", - "ListToolsResponse", - "Member", - "MemberRole", - "MembersListResponse", - "Memory", - "Message", - "MessageRole", - "MockingStrategy", - "NestedChunk", - "NoMatchBehavior", - "OAuthError", - "OAuthErrorError", - "ParameterCheck", - "ParameterCheckMode", - "ParameterCheckResult", - "RunAgentTestsResponse", - "RunBatchResponse", - "ScenarioConfig", - "ScenarioResult", - "SearchKnowledgeBasesResponse", - "SimulationConfig", - "SimulationMessage", - "SimulationMessageRole", - "SimulationResult", - "SimulationToolCall", - "SpeechMarks", - "SystemToolConfig", - "SystemToolConfigBuiltin", - "SystemVariableDoc", - "Tenant", - "TenantDataRegion", - "TenantPlan", - "TenantsListResponse", - "TestRunResult", - "TestRunStatus", - "TestStats", - "TestStatsBucket", - "TestType", - "Tool", - "ToolCallConfig", - "ToolCallResult", - "ToolConfig", - "ToolKind", - "ToolMock", - "ToolMockConfig", - "ToolParam", - "ToolParamType", - "WebhookToolConfig", - "WebhookToolConfigMethod", -] diff --git a/src/speechify/tts/types/access_token.py b/src/speechify/tts/types/access_token.py deleted file mode 100644 index 52dbb9d..0000000 --- a/src/speechify/tts/types/access_token.py +++ /dev/null @@ -1,35 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -import pydantic -from .access_token_scope import AccessTokenScope -from .access_token_token_type import AccessTokenTokenType -from ...core.pydantic_utilities import IS_PYDANTIC_V2 - - -class AccessToken(UniversalBaseModel): - access_token: typing.Optional[str] = None - expires_in: typing.Optional[int] = pydantic.Field(default=None) - """ - Expiration time, in seconds from the issue time - """ - - scope: typing.Optional[AccessTokenScope] = pydantic.Field(default=None) - """ - The scope, or a space-delimited list of scopes the token is issued for - """ - - token_type: typing.Optional[AccessTokenTokenType] = pydantic.Field(default=None) - """ - Token type - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/access_token_scope.py b/src/speechify/tts/types/access_token_scope.py deleted file mode 100644 index 73cae3b..0000000 --- a/src/speechify/tts/types/access_token_scope.py +++ /dev/null @@ -1,10 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -AccessTokenScope = typing.Union[ - typing.Literal[ - "audio:speech", "audio:stream", "audio:all", "voices:read", "voices:create", "voices:delete", "voices:all" - ], - typing.Any, -] diff --git a/src/speechify/tts/types/access_token_token_type.py b/src/speechify/tts/types/access_token_token_type.py deleted file mode 100644 index 065f512..0000000 --- a/src/speechify/tts/types/access_token_token_type.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -AccessTokenTokenType = typing.Union[typing.Literal["bearer"], typing.Any] diff --git a/src/speechify/tts/types/agent.py b/src/speechify/tts/types/agent.py deleted file mode 100644 index 13a28c4..0000000 --- a/src/speechify/tts/types/agent.py +++ /dev/null @@ -1,87 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -import pydantic -import datetime as dt -from ...core.pydantic_utilities import IS_PYDANTIC_V2 - - -class Agent(UniversalBaseModel): - id: str - name: str - slug: str - prompt: typing.Optional[str] = None - first_message: typing.Optional[str] = None - language: str = pydantic.Field() - """ - ISO 639-1 code, e.g. 'en'. - """ - - llm_model: str = pydantic.Field() - """ - Chat model slug. Leave empty to use the Speechify default. - """ - - voice_id: str = pydantic.Field() - """ - Speechify voice slug. - """ - - temperature: float - config: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = pydantic.Field(default=None) - """ - Free-form agent config JSON (evaluation_config is read via its own endpoint). - """ - - is_public: bool = pydantic.Field() - """ - When true, the `` web component can start a - session against this agent without an API key, subject to - the `allowed_origins` allowlist. When false (default), only - authenticated callers can start sessions. - """ - - allowed_origins: typing.List[str] = pydantic.Field() - """ - Exact `Origin` header values (e.g. `https://example.com`) - that are allowed to start public sessions. Empty array - with `is_public = true` means any origin is accepted — - intended for open demos. No subdomain wildcards. - """ - - hostname_allowlist: typing.Optional[typing.List[str]] = pydantic.Field(default=None) - """ - Optional per-agent hostname allowlist enforced at - session-creation time. When set and non-empty, the - `Origin` header's hostname must be an exact member. - Bare hostnames only — no scheme, port, or path. Up to - 10 entries. Omit (null) or leave empty for no - enforcement (public agents accept any hostname). - """ - - memory_enabled: bool = pydantic.Field() - """ - When true, the post-call extractor writes durable facts about - each caller; at conversation-start the retriever injects the - top matches into the system prompt via the `{{memory}}` - template variable. Defaults to false. - """ - - memory_retention_days: int = pydantic.Field() - """ - Maximum age (in days) of memories kept and surfaced to the - retriever. 0 disables the cap. Defaults to 90. - """ - - created_at: dt.datetime - updated_at: dt.datetime - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/agent_test.py b/src/speechify/tts/types/agent_test.py deleted file mode 100644 index 9b7cf6c..0000000 --- a/src/speechify/tts/types/agent_test.py +++ /dev/null @@ -1,57 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -from .test_type import TestType -from .agent_test_config import AgentTestConfig -import pydantic -import typing -from .tool_mock_config import ToolMockConfig -import datetime as dt -from ...core.pydantic_utilities import IS_PYDANTIC_V2 - - -class AgentTest(UniversalBaseModel): - """ - A configured test against a voice agent. `config` is a - type-specific document - see `ScenarioConfig`, `ToolCallConfig`, - and `SimulationConfig` for the per-type shapes (discriminated by `type`). - """ - - id: str - agent_id: str - name: str - description: str - type: TestType - config: AgentTestConfig = pydantic.Field() - """ - Type-specific configuration document. - """ - - tool_mock_config: typing.Optional[ToolMockConfig] = pydantic.Field(default=None) - """ - Optional tool-mocking config applied during runs of this test. - """ - - variables: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = pydantic.Field(default=None) - """ - Per-test dynamic-variable overrides. Keys substitute `{{key}}` - placeholders inside the test config at run-start. Unknown keys - render as empty string, matching session dispatch behaviour. - """ - - folder_id: typing.Optional[str] = pydantic.Field(default=None) - """ - Folder the test belongs to; null = root (unfiled). - """ - - created_at: dt.datetime - updated_at: dt.datetime - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/agent_test_attachment.py b/src/speechify/tts/types/agent_test_attachment.py deleted file mode 100644 index 3a1f30f..0000000 --- a/src/speechify/tts/types/agent_test_attachment.py +++ /dev/null @@ -1,26 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import datetime as dt -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import typing -import pydantic - - -class AgentTestAttachment(UniversalBaseModel): - """ - One (test, agent) pair. Poll the `attached_agent_ids` field on `AgentTestWithLastRun` or hit `/v1/tests/{id}/attachments` for the authoritative set. - """ - - test_id: str - agent_id: str - created_at: dt.datetime - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/agent_test_config.py b/src/speechify/tts/types/agent_test_config.py deleted file mode 100644 index d0ce190..0000000 --- a/src/speechify/tts/types/agent_test_config.py +++ /dev/null @@ -1,8 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from .scenario_config import ScenarioConfig -from .tool_call_config import ToolCallConfig -from .simulation_config import SimulationConfig - -AgentTestConfig = typing.Union[ScenarioConfig, ToolCallConfig, SimulationConfig] diff --git a/src/speechify/tts/types/agent_test_folder.py b/src/speechify/tts/types/agent_test_folder.py deleted file mode 100644 index 88cc31d..0000000 --- a/src/speechify/tts/types/agent_test_folder.py +++ /dev/null @@ -1,28 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -import datetime as dt -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import pydantic - - -class AgentTestFolder(UniversalBaseModel): - """ - One organisational node in the per-owner tests tree. - """ - - id: str - parent_folder_id: typing.Optional[str] = None - name: str - created_at: dt.datetime - updated_at: dt.datetime - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/agent_test_run.py b/src/speechify/tts/types/agent_test_run.py deleted file mode 100644 index 3f51754..0000000 --- a/src/speechify/tts/types/agent_test_run.py +++ /dev/null @@ -1,44 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -from .test_run_status import TestRunStatus -import typing -import datetime as dt -from .test_run_result import TestRunResult -import pydantic -from ...core.pydantic_utilities import IS_PYDANTIC_V2 - - -class AgentTestRun(UniversalBaseModel): - """ - One execution of a test. `result` is populated when `status` - reaches a terminal state (`passed`, `failed`, or `error`). - See `TestRunResult` for the shape. - """ - - id: str - test_id: str - agent_id: str - status: TestRunStatus - started_at: typing.Optional[dt.datetime] = None - completed_at: typing.Optional[dt.datetime] = None - result: typing.Optional[TestRunResult] = pydantic.Field(default=None) - """ - Populated on terminal status only. - """ - - error: typing.Optional[str] = pydantic.Field(default=None) - """ - Human-readable error message when status is `error`. - """ - - created_at: dt.datetime - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/agent_test_with_last_run.py b/src/speechify/tts/types/agent_test_with_last_run.py deleted file mode 100644 index cb22c0b..0000000 --- a/src/speechify/tts/types/agent_test_with_last_run.py +++ /dev/null @@ -1,36 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .agent_test import AgentTest -import typing -from .agent_test_run import AgentTestRun -import pydantic -from ...core.pydantic_utilities import IS_PYDANTIC_V2 - - -class AgentTestWithLastRun(AgentTest): - """ - List-view projection of a test that includes the most recent run - so the console can display pass/fail badges without an extra - round-trip. On the global `/v1/tests` surface, also carries - `attached_agent_ids` so the row can render agent chips without a - follow-up request. - """ - - last_run: typing.Optional[AgentTestRun] = pydantic.Field(default=None) - """ - The most recent run, or null if the test has never been run. - """ - - attached_agent_ids: typing.Optional[typing.List[str]] = pydantic.Field(default=None) - """ - Every agent this test runs against. Always includes the owner agent. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/api_key.py b/src/speechify/tts/types/api_key.py deleted file mode 100644 index ce1b612..0000000 --- a/src/speechify/tts/types/api_key.py +++ /dev/null @@ -1,47 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -import pydantic -from ...core.pydantic_utilities import IS_PYDANTIC_V2 - - -class ApiKey(UniversalBaseModel): - api_key: typing.Optional[str] = pydantic.Field(default=None) - """ - API key - """ - - created_at: typing.Optional[int] = pydantic.Field(default=None) - """ - Creation time of the key - """ - - id: typing.Optional[int] = pydantic.Field(default=None) - """ - ID of the key - """ - - name: typing.Optional[str] = pydantic.Field(default=None) - """ - Name of the key - """ - - updated_at: typing.Optional[int] = pydantic.Field(default=None) - """ - Last updated time of the key - """ - - user_id: typing.Optional[str] = pydantic.Field(default=None) - """ - User ID to whom the key belongs - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/batch_run_entry.py b/src/speechify/tts/types/batch_run_entry.py deleted file mode 100644 index 7ba49b3..0000000 --- a/src/speechify/tts/types/batch_run_entry.py +++ /dev/null @@ -1,25 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import pydantic - - -class BatchRunEntry(UniversalBaseModel): - """ - One entry in a batch-run request. Omit `agent_id` to fan out to - every agent the test is attached to. - """ - - test_id: str - agent_id: typing.Optional[str] = None - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/client_tool_config.py b/src/speechify/tts/types/client_tool_config.py deleted file mode 100644 index 5966aaa..0000000 --- a/src/speechify/tts/types/client_tool_config.py +++ /dev/null @@ -1,25 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -from .tool_param import ToolParam -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import pydantic - - -class ClientToolConfig(UniversalBaseModel): - """ - Config shape for `kind=client`. Execution happens in the caller's browser / SDK. - """ - - params: typing.Optional[typing.List[ToolParam]] = None - timeout_ms: typing.Optional[int] = None - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/conversation.py b/src/speechify/tts/types/conversation.py deleted file mode 100644 index e198694..0000000 --- a/src/speechify/tts/types/conversation.py +++ /dev/null @@ -1,40 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -from .conversation_status import ConversationStatus -from .conversation_transport import ConversationTransport -import datetime as dt -import pydantic -from ...core.pydantic_utilities import IS_PYDANTIC_V2 - - -class Conversation(UniversalBaseModel): - id: str - agent_id: str - room_name: str - room_sid: typing.Optional[str] = None - status: ConversationStatus - transport: ConversationTransport - started_at: typing.Optional[dt.datetime] = pydantic.Field(default=None) - """ - Set when the first user participant joins the realtime - voice session. Null between CreateConversation and the - participant-joined event, and stays null if no user ever - joins. - """ - - ended_at: typing.Optional[dt.datetime] = None - duration_ms: typing.Optional[int] = None - cost_cents: typing.Optional[int] = None - recording_url: typing.Optional[str] = None - metadata: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = None - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/conversation_stats.py b/src/speechify/tts/types/conversation_stats.py deleted file mode 100644 index 011243f..0000000 --- a/src/speechify/tts/types/conversation_stats.py +++ /dev/null @@ -1,29 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import pydantic - - -class ConversationStats(UniversalBaseModel): - """ - Counts + averages over the caller's conversations matching the supplied filters. AVG fields are null when no rows match the FILTER predicate. - """ - - total: int - completed: int - failed: int - active: int - pending: int - avg_duration_ms: typing.Optional[float] = None - avg_cost_cents: typing.Optional[float] = None - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/conversation_status.py b/src/speechify/tts/types/conversation_status.py deleted file mode 100644 index 7b121bf..0000000 --- a/src/speechify/tts/types/conversation_status.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -ConversationStatus = typing.Union[typing.Literal["pending", "active", "completed", "failed"], typing.Any] diff --git a/src/speechify/tts/types/conversation_transport.py b/src/speechify/tts/types/conversation_transport.py deleted file mode 100644 index 4451eb9..0000000 --- a/src/speechify/tts/types/conversation_transport.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -ConversationTransport = typing.Union[typing.Literal["web", "sip"], typing.Any] diff --git a/src/speechify/tts/types/create_conversation_response.py b/src/speechify/tts/types/create_conversation_response.py deleted file mode 100644 index cc53806..0000000 --- a/src/speechify/tts/types/create_conversation_response.py +++ /dev/null @@ -1,37 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -from .conversation import Conversation -import pydantic -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import typing - - -class CreateConversationResponse(UniversalBaseModel): - """ - Returned when a conversation is created. The `token` + `url` - let the caller connect its browser/SDK directly to the - realtime voice session — the agent that answers is dispatched - server-side. - """ - - conversation: Conversation - room: str - token: str = pydantic.Field() - """ - Short-lived realtime session access token (JWT). - """ - - url: str = pydantic.Field() - """ - Realtime session wss:// URL to connect to. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/create_voice_model_name.py b/src/speechify/tts/types/create_voice_model_name.py deleted file mode 100644 index 34c21b3..0000000 --- a/src/speechify/tts/types/create_voice_model_name.py +++ /dev/null @@ -1,7 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -CreateVoiceModelName = typing.Union[ - typing.Literal["simba-base", "simba-english", "simba-multilingual", "simba-turbo", "simba-3.0"], typing.Any -] diff --git a/src/speechify/tts/types/data_collection_field.py b/src/speechify/tts/types/data_collection_field.py deleted file mode 100644 index 1f55f6b..0000000 --- a/src/speechify/tts/types/data_collection_field.py +++ /dev/null @@ -1,28 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -from .data_collection_field_type import DataCollectionFieldType -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import typing -import pydantic - - -class DataCollectionField(UniversalBaseModel): - """ - A structured value the post-call evaluator should extract from the - transcript. `int` is distinct from `number` so downstream consumers - receive whole integers without a synthetic decimal. - """ - - key: str - description: str - type: DataCollectionFieldType - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/data_collection_field_type.py b/src/speechify/tts/types/data_collection_field_type.py deleted file mode 100644 index b9a1a96..0000000 --- a/src/speechify/tts/types/data_collection_field_type.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -DataCollectionFieldType = typing.Union[typing.Literal["string", "int", "number", "boolean"], typing.Any] diff --git a/src/speechify/tts/types/delete_memories_by_caller_response.py b/src/speechify/tts/types/delete_memories_by_caller_response.py deleted file mode 100644 index f7fdf5b..0000000 --- a/src/speechify/tts/types/delete_memories_by_caller_response.py +++ /dev/null @@ -1,22 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import pydantic -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import typing - - -class DeleteMemoriesByCallerResponse(UniversalBaseModel): - deleted: int = pydantic.Field() - """ - Number of memories soft-deleted. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/dynamic_variable.py b/src/speechify/tts/types/dynamic_variable.py deleted file mode 100644 index aab728b..0000000 --- a/src/speechify/tts/types/dynamic_variable.py +++ /dev/null @@ -1,43 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import pydantic -from .dynamic_variable_type import DynamicVariableType -import typing -from ...core.pydantic_utilities import IS_PYDANTIC_V2 - - -class DynamicVariable(UniversalBaseModel): - """ - One customer-scope variable definition on an agent. Referenced in - prompts, first messages, and webhook tool configs via `{{key}}` or - `{{key|json}}`. Missing variables render as empty string at dispatch - time - a typo never breaks a session. - """ - - key: str = pydantic.Field() - """ - Variable name. Must match `[a-zA-Z0-9_]+`. The `system__` prefix - is reserved for platform-populated variables and will be rejected. - """ - - type: DynamicVariableType - default: typing.Optional[typing.Optional[typing.Any]] = pydantic.Field(default=None) - """ - Optional default value used when no per-session override is - supplied. Must conform to the declared `type`. - """ - - description: typing.Optional[str] = pydantic.Field(default=None) - """ - Human-readable note shown in the console variable editor. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/dynamic_variable_type.py b/src/speechify/tts/types/dynamic_variable_type.py deleted file mode 100644 index d12f3f8..0000000 --- a/src/speechify/tts/types/dynamic_variable_type.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -DynamicVariableType = typing.Union[typing.Literal["string", "number", "boolean", "json"], typing.Any] diff --git a/src/speechify/tts/types/evaluation.py b/src/speechify/tts/types/evaluation.py deleted file mode 100644 index 06414f1..0000000 --- a/src/speechify/tts/types/evaluation.py +++ /dev/null @@ -1,51 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -from .evaluation_kind import EvaluationKind -import typing -from .evaluation_status import EvaluationStatus -import pydantic -import datetime as dt -from ...core.pydantic_utilities import IS_PYDANTIC_V2 - - -class Evaluation(UniversalBaseModel): - """ - Three flavours coexist, discriminated by `kind`: - - `criterion` rows carry `status` + `passed` + `score` + `rationale` for one criterion - - `summary` row carries overall sentiment + rationale in `rationale` - - `data` row carries the structured data-collection payload in `data` - - `status` is the canonical three-state result. `passed` is a - derived boolean kept for backwards compatibility with earlier - webhook consumers: success→true, failure→false, unknown→null. - """ - - id: str - conversation_id: str - kind: EvaluationKind - criterion_id: typing.Optional[str] = None - name: str - status: typing.Optional[EvaluationStatus] = pydantic.Field(default=None) - """ - Three-state criterion result. `unknown` means the criterion did not apply to this call. - """ - - passed: typing.Optional[bool] = None - score: typing.Optional[float] = None - rationale: str - data: typing.Optional[typing.Optional[typing.Any]] = pydantic.Field(default=None) - """ - Structured data-collection payload (present only on `kind=data` rows). - """ - - created_at: dt.datetime - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/evaluation_config.py b/src/speechify/tts/types/evaluation_config.py deleted file mode 100644 index cc757ed..0000000 --- a/src/speechify/tts/types/evaluation_config.py +++ /dev/null @@ -1,22 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -from .evaluation_criterion import EvaluationCriterion -from .data_collection_field import DataCollectionField -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import pydantic - - -class EvaluationConfig(UniversalBaseModel): - criteria: typing.List[EvaluationCriterion] - data_collection: typing.List[DataCollectionField] - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/evaluation_criterion.py b/src/speechify/tts/types/evaluation_criterion.py deleted file mode 100644 index 235443b..0000000 --- a/src/speechify/tts/types/evaluation_criterion.py +++ /dev/null @@ -1,25 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import typing -import pydantic - - -class EvaluationCriterion(UniversalBaseModel): - """ - One LLM-scored assertion about the call ("Did the agent confirm the customer's name?"). - """ - - id: str - name: str - description: str - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/evaluation_kind.py b/src/speechify/tts/types/evaluation_kind.py deleted file mode 100644 index 8c1feac..0000000 --- a/src/speechify/tts/types/evaluation_kind.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -EvaluationKind = typing.Union[typing.Literal["criterion", "summary", "data"], typing.Any] diff --git a/src/speechify/tts/types/evaluation_status.py b/src/speechify/tts/types/evaluation_status.py deleted file mode 100644 index 9af14a4..0000000 --- a/src/speechify/tts/types/evaluation_status.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -EvaluationStatus = typing.Union[typing.Literal["success", "failure", "unknown"], typing.Any] diff --git a/src/speechify/tts/types/get_voices_model_name.py b/src/speechify/tts/types/get_voices_model_name.py deleted file mode 100644 index 121de6e..0000000 --- a/src/speechify/tts/types/get_voices_model_name.py +++ /dev/null @@ -1,7 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -GetVoicesModelName = typing.Union[ - typing.Literal["simba-base", "simba-english", "simba-multilingual", "simba-turbo"], typing.Any -] diff --git a/src/speechify/tts/types/invite.py b/src/speechify/tts/types/invite.py deleted file mode 100644 index 3314199..0000000 --- a/src/speechify/tts/types/invite.py +++ /dev/null @@ -1,56 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import pydantic -import datetime as dt -import typing -from ...core.pydantic_utilities import IS_PYDANTIC_V2 - - -class Invite(UniversalBaseModel): - """ - A pending or historical workspace invite. - """ - - id: str = pydantic.Field() - """ - Opaque invite ID. - """ - - email: str = pydantic.Field() - """ - Invitee email. - """ - - invited_by: str = pydantic.Field() - """ - Firebase UID of the member who created the invite. - """ - - created_at: dt.datetime - expires_at: dt.datetime - accepted_at: typing.Optional[dt.datetime] = pydantic.Field(default=None) - """ - Populated once the invite has been accepted. - """ - - revoked_at: typing.Optional[dt.datetime] = pydantic.Field(default=None) - """ - Populated once the invite has been revoked. - """ - - token: typing.Optional[str] = pydantic.Field(default=None) - """ - Invite token. Returned ONLY on the create-invite response; - subsequent list calls redact it. Use the token to build the - `/join/{token}` join URL. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/invite_preview.py b/src/speechify/tts/types/invite_preview.py deleted file mode 100644 index 0a9d2ca..0000000 --- a/src/speechify/tts/types/invite_preview.py +++ /dev/null @@ -1,54 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import pydantic -import typing -import datetime as dt -from ...core.pydantic_utilities import IS_PYDANTIC_V2 - - -class InvitePreview(UniversalBaseModel): - """ - Unauthenticated preview of a workspace invite. Surfaces only what - the recipient needs to decide whether to accept (workspace name, - invited address, inviter, expiry). Billing, plan, data region, - and invite token are deliberately omitted. - """ - - tenant_id: str = pydantic.Field() - """ - Opaque workspace id. Safe to echo back on the accept call. - """ - - tenant_name: str = pydantic.Field() - """ - Workspace display name. - """ - - invited_email: str = pydantic.Field() - """ - The email address the inviter typed when creating the invite. - """ - - invited_by_email: typing.Optional[str] = pydantic.Field(default=None) - """ - Firebase email of the member who created the invite. May be - absent if the Firebase profile lookup failed transiently — - clients should still render the preview in that case. - """ - - invited_by_display_name: typing.Optional[str] = pydantic.Field(default=None) - """ - Firebase display name of the member who created the invite. - """ - - expires_at: dt.datetime - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/invites_list_response.py b/src/speechify/tts/types/invites_list_response.py deleted file mode 100644 index 640fcc9..0000000 --- a/src/speechify/tts/types/invites_list_response.py +++ /dev/null @@ -1,20 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -from .invite import Invite -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import pydantic - - -class InvitesListResponse(UniversalBaseModel): - invites: typing.List[Invite] - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/knowledge_base.py b/src/speechify/tts/types/knowledge_base.py deleted file mode 100644 index 55add05..0000000 --- a/src/speechify/tts/types/knowledge_base.py +++ /dev/null @@ -1,43 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import pydantic -import datetime as dt -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import typing - - -class KnowledgeBase(UniversalBaseModel): - """ - A bundle of documents that can be attached to one or more voice - agents. Chunks across every document in the knowledge base are - embedded and searched together. - """ - - id: str - name: str = pydantic.Field() - """ - Human-readable label, shown in the console. - """ - - description: str = pydantic.Field() - """ - Optional description. - """ - - document_count: int = pydantic.Field() - """ - Number of ingested documents. - """ - - created_at: dt.datetime - updated_at: dt.datetime - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/knowledge_base_chunk.py b/src/speechify/tts/types/knowledge_base_chunk.py deleted file mode 100644 index c0e2081..0000000 --- a/src/speechify/tts/types/knowledge_base_chunk.py +++ /dev/null @@ -1,23 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import typing -import pydantic - - -class KnowledgeBaseChunk(UniversalBaseModel): - id: str - document_id: str - kb_id: str - chunk_index: int - content: str - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/knowledge_base_document.py b/src/speechify/tts/types/knowledge_base_document.py deleted file mode 100644 index bcf1ae3..0000000 --- a/src/speechify/tts/types/knowledge_base_document.py +++ /dev/null @@ -1,35 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -from .knowledge_base_document_status import KnowledgeBaseDocumentStatus -import typing -import pydantic -import datetime as dt -from ...core.pydantic_utilities import IS_PYDANTIC_V2 - - -class KnowledgeBaseDocument(UniversalBaseModel): - id: str - kb_id: str - filename: str - content_type: str - byte_size: int - char_count: int - chunk_count: int - status: KnowledgeBaseDocumentStatus - error: typing.Optional[str] = pydantic.Field(default=None) - """ - Populated when status is failed. - """ - - created_at: dt.datetime - updated_at: dt.datetime - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/knowledge_base_document_status.py b/src/speechify/tts/types/knowledge_base_document_status.py deleted file mode 100644 index 35fb962..0000000 --- a/src/speechify/tts/types/knowledge_base_document_status.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -KnowledgeBaseDocumentStatus = typing.Union[typing.Literal["embedding", "ready", "failed"], typing.Any] diff --git a/src/speechify/tts/types/knowledge_base_search_hit.py b/src/speechify/tts/types/knowledge_base_search_hit.py deleted file mode 100644 index 4422827..0000000 --- a/src/speechify/tts/types/knowledge_base_search_hit.py +++ /dev/null @@ -1,28 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import pydantic -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import typing - - -class KnowledgeBaseSearchHit(UniversalBaseModel): - chunk_id: str - document_id: str - kb_id: str - filename: str - chunk_index: int - content: str - score: float = pydantic.Field() - """ - Cosine similarity (higher = more relevant). - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/list_agent_test_attachments_response.py b/src/speechify/tts/types/list_agent_test_attachments_response.py deleted file mode 100644 index 0f4287a..0000000 --- a/src/speechify/tts/types/list_agent_test_attachments_response.py +++ /dev/null @@ -1,20 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -from .agent_test_attachment import AgentTestAttachment -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import pydantic - - -class ListAgentTestAttachmentsResponse(UniversalBaseModel): - attachments: typing.List[AgentTestAttachment] - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/list_agent_test_folders_response.py b/src/speechify/tts/types/list_agent_test_folders_response.py deleted file mode 100644 index 553d728..0000000 --- a/src/speechify/tts/types/list_agent_test_folders_response.py +++ /dev/null @@ -1,20 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -from .agent_test_folder import AgentTestFolder -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import pydantic - - -class ListAgentTestFoldersResponse(UniversalBaseModel): - folders: typing.List[AgentTestFolder] - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/list_agent_test_runs_response.py b/src/speechify/tts/types/list_agent_test_runs_response.py deleted file mode 100644 index 27c484b..0000000 --- a/src/speechify/tts/types/list_agent_test_runs_response.py +++ /dev/null @@ -1,20 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -from .agent_test_run import AgentTestRun -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import pydantic - - -class ListAgentTestRunsResponse(UniversalBaseModel): - runs: typing.List[AgentTestRun] - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/list_agent_tests_response.py b/src/speechify/tts/types/list_agent_tests_response.py deleted file mode 100644 index 41e6bdd..0000000 --- a/src/speechify/tts/types/list_agent_tests_response.py +++ /dev/null @@ -1,20 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -from .agent_test_with_last_run import AgentTestWithLastRun -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import pydantic - - -class ListAgentTestsResponse(UniversalBaseModel): - tests: typing.List[AgentTestWithLastRun] - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/list_agents_response.py b/src/speechify/tts/types/list_agents_response.py deleted file mode 100644 index 757f14d..0000000 --- a/src/speechify/tts/types/list_agents_response.py +++ /dev/null @@ -1,20 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -from .agent import Agent -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import pydantic - - -class ListAgentsResponse(UniversalBaseModel): - agents: typing.List[Agent] - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/list_conversations_response.py b/src/speechify/tts/types/list_conversations_response.py deleted file mode 100644 index 79c0d2a..0000000 --- a/src/speechify/tts/types/list_conversations_response.py +++ /dev/null @@ -1,20 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -from .conversation import Conversation -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import pydantic - - -class ListConversationsResponse(UniversalBaseModel): - conversations: typing.List[Conversation] - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/list_dynamic_variables_response.py b/src/speechify/tts/types/list_dynamic_variables_response.py deleted file mode 100644 index 2cf72f8..0000000 --- a/src/speechify/tts/types/list_dynamic_variables_response.py +++ /dev/null @@ -1,36 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -from .dynamic_variable import DynamicVariable -import pydantic -from .system_variable_doc import SystemVariableDoc -from ...core.pydantic_utilities import IS_PYDANTIC_V2 - - -class ListDynamicVariablesResponse(UniversalBaseModel): - """ - Response for `GET /v1/agents/{id}/variables`. Returns both the - customer-scope variable catalogue and the read-only `system__*` - catalogue so the editor UI has a single source of truth. - """ - - variables: typing.List[DynamicVariable] = pydantic.Field() - """ - Customer-defined variables for this agent. - """ - - system_variables: typing.List[SystemVariableDoc] = pydantic.Field() - """ - Platform-populated `system__*` variables, provided for - reference. This list is the same for every agent. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/list_evaluations_response.py b/src/speechify/tts/types/list_evaluations_response.py deleted file mode 100644 index 79d9b91..0000000 --- a/src/speechify/tts/types/list_evaluations_response.py +++ /dev/null @@ -1,20 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -from .evaluation import Evaluation -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import pydantic - - -class ListEvaluationsResponse(UniversalBaseModel): - evaluations: typing.List[Evaluation] - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/list_knowledge_base_chunks_response.py b/src/speechify/tts/types/list_knowledge_base_chunks_response.py deleted file mode 100644 index 9324636..0000000 --- a/src/speechify/tts/types/list_knowledge_base_chunks_response.py +++ /dev/null @@ -1,20 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -from .knowledge_base_chunk import KnowledgeBaseChunk -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import pydantic - - -class ListKnowledgeBaseChunksResponse(UniversalBaseModel): - chunks: typing.List[KnowledgeBaseChunk] - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/list_knowledge_base_documents_response.py b/src/speechify/tts/types/list_knowledge_base_documents_response.py deleted file mode 100644 index 15d69ed..0000000 --- a/src/speechify/tts/types/list_knowledge_base_documents_response.py +++ /dev/null @@ -1,20 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -from .knowledge_base_document import KnowledgeBaseDocument -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import pydantic - - -class ListKnowledgeBaseDocumentsResponse(UniversalBaseModel): - documents: typing.List[KnowledgeBaseDocument] - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/list_knowledge_bases_response.py b/src/speechify/tts/types/list_knowledge_bases_response.py deleted file mode 100644 index acb38d7..0000000 --- a/src/speechify/tts/types/list_knowledge_bases_response.py +++ /dev/null @@ -1,20 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -from .knowledge_base import KnowledgeBase -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import pydantic - - -class ListKnowledgeBasesResponse(UniversalBaseModel): - knowledge_bases: typing.List[KnowledgeBase] - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/list_memories_response.py b/src/speechify/tts/types/list_memories_response.py deleted file mode 100644 index 79b426b..0000000 --- a/src/speechify/tts/types/list_memories_response.py +++ /dev/null @@ -1,20 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -from .memory import Memory -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import pydantic - - -class ListMemoriesResponse(UniversalBaseModel): - memories: typing.List[Memory] - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/list_messages_response.py b/src/speechify/tts/types/list_messages_response.py deleted file mode 100644 index 1a71747..0000000 --- a/src/speechify/tts/types/list_messages_response.py +++ /dev/null @@ -1,20 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -from .message import Message -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import pydantic - - -class ListMessagesResponse(UniversalBaseModel): - messages: typing.List[Message] - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/list_tests_response.py b/src/speechify/tts/types/list_tests_response.py deleted file mode 100644 index 144048b..0000000 --- a/src/speechify/tts/types/list_tests_response.py +++ /dev/null @@ -1,27 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -from .agent_test_with_last_run import AgentTestWithLastRun -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import pydantic - - -class ListTestsResponse(UniversalBaseModel): - """ - Workspace-wide paginated list of tests. `next_cursor` is the opaque - page cursor; omit on the first call, then pass through to get the - next page until the field is absent. - """ - - tests: typing.List[AgentTestWithLastRun] - next_cursor: typing.Optional[str] = None - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/list_tools_response.py b/src/speechify/tts/types/list_tools_response.py deleted file mode 100644 index bb7bb05..0000000 --- a/src/speechify/tts/types/list_tools_response.py +++ /dev/null @@ -1,20 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -from .tool import Tool -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import pydantic - - -class ListToolsResponse(UniversalBaseModel): - tools: typing.List[Tool] - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/member.py b/src/speechify/tts/types/member.py deleted file mode 100644 index 158f541..0000000 --- a/src/speechify/tts/types/member.py +++ /dev/null @@ -1,49 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import pydantic -import typing -from .member_role import MemberRole -import datetime as dt -from ...core.pydantic_utilities import IS_PYDANTIC_V2 - - -class Member(UniversalBaseModel): - """ - A member of a workspace (joined from `tenant_users` + Firebase profile). - """ - - user_uid: str = pydantic.Field() - """ - Firebase user ID. - """ - - email: typing.Optional[str] = pydantic.Field(default=None) - """ - Member's email from Firebase. Empty when the account has been deleted. - """ - - display_name: typing.Optional[str] = pydantic.Field(default=None) - """ - Member's display name from Firebase. - """ - - role: MemberRole - created_at: dt.datetime = pydantic.Field() - """ - When the user joined the workspace. - """ - - is_self: bool = pydantic.Field() - """ - True when this row is the authenticated caller. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/member_role.py b/src/speechify/tts/types/member_role.py deleted file mode 100644 index 13def0f..0000000 --- a/src/speechify/tts/types/member_role.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -MemberRole = typing.Union[typing.Literal["owner", "admin", "member"], typing.Any] diff --git a/src/speechify/tts/types/members_list_response.py b/src/speechify/tts/types/members_list_response.py deleted file mode 100644 index 819d86d..0000000 --- a/src/speechify/tts/types/members_list_response.py +++ /dev/null @@ -1,20 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -from .member import Member -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import pydantic - - -class MembersListResponse(UniversalBaseModel): - members: typing.List[Member] - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/memory.py b/src/speechify/tts/types/memory.py deleted file mode 100644 index 373c863..0000000 --- a/src/speechify/tts/types/memory.py +++ /dev/null @@ -1,54 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import pydantic -import typing -import datetime as dt -from ...core.pydantic_utilities import IS_PYDANTIC_V2 - - -class Memory(UniversalBaseModel): - """ - One salient fact extracted post-call about a specific caller on - a specific agent. Retrieved at the next conversation-start for - the same caller and injected into the agent's system prompt via - the `{{memory}}` template variable. - """ - - id: str - agent_id: str - caller_identity: str = pydantic.Field() - """ - Stable caller key (LiveKit participant identity) the memory is scoped to. - """ - - fact: str = pydantic.Field() - """ - Short third-person statement about the caller. - """ - - source_conversation_id: typing.Optional[str] = pydantic.Field(default=None) - """ - Conversation the memory was extracted from (may be empty if the source was deleted). - """ - - confidence: float = pydantic.Field() - """ - LLM self-reported 0-1 confidence in the fact's durability and relevance. - """ - - score: typing.Optional[float] = pydantic.Field(default=None) - """ - Populated only on retrieval hits — recency-weighted cosine similarity. - """ - - created_at: dt.datetime - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/message.py b/src/speechify/tts/types/message.py deleted file mode 100644 index 2112717..0000000 --- a/src/speechify/tts/types/message.py +++ /dev/null @@ -1,33 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -from .message_role import MessageRole -import typing -import pydantic -import datetime as dt -from ...core.pydantic_utilities import IS_PYDANTIC_V2 - - -class Message(UniversalBaseModel): - id: str - conversation_id: str - role: MessageRole - content: str - tool_name: typing.Optional[str] = None - tool_args: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = None - tool_result: typing.Optional[typing.Optional[typing.Any]] = pydantic.Field(default=None) - """ - Arbitrary JSON value returned by the tool (object, array, string, or primitive). - """ - - started_at: dt.datetime - ended_at: typing.Optional[dt.datetime] = None - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/message_role.py b/src/speechify/tts/types/message_role.py deleted file mode 100644 index decda98..0000000 --- a/src/speechify/tts/types/message_role.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -MessageRole = typing.Union[typing.Literal["user", "assistant", "system", "tool"], typing.Any] diff --git a/src/speechify/tts/types/mocking_strategy.py b/src/speechify/tts/types/mocking_strategy.py deleted file mode 100644 index 344c9be..0000000 --- a/src/speechify/tts/types/mocking_strategy.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -MockingStrategy = typing.Union[typing.Literal["none", "all", "selected"], typing.Any] diff --git a/src/speechify/tts/types/no_match_behavior.py b/src/speechify/tts/types/no_match_behavior.py deleted file mode 100644 index 29c5374..0000000 --- a/src/speechify/tts/types/no_match_behavior.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -NoMatchBehavior = typing.Union[typing.Literal["call_real_tool", "finish_with_error", "skip"], typing.Any] diff --git a/src/speechify/tts/types/o_auth_error.py b/src/speechify/tts/types/o_auth_error.py deleted file mode 100644 index 8eaf2b7..0000000 --- a/src/speechify/tts/types/o_auth_error.py +++ /dev/null @@ -1,21 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -from .o_auth_error_error import OAuthErrorError -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import pydantic - - -class OAuthError(UniversalBaseModel): - error: typing.Optional[OAuthErrorError] = None - error_description: typing.Optional[str] = None - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/o_auth_error_error.py b/src/speechify/tts/types/o_auth_error_error.py deleted file mode 100644 index 3dcbfaf..0000000 --- a/src/speechify/tts/types/o_auth_error_error.py +++ /dev/null @@ -1,10 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -OAuthErrorError = typing.Union[ - typing.Literal[ - "invalid_client", "unauthorized_client", "invalid_request", "unsupported_grant_type", "invalid_scope" - ], - typing.Any, -] diff --git a/src/speechify/tts/types/parameter_check.py b/src/speechify/tts/types/parameter_check.py deleted file mode 100644 index 773a769..0000000 --- a/src/speechify/tts/types/parameter_check.py +++ /dev/null @@ -1,41 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import pydantic -from .parameter_check_mode import ParameterCheckMode -import typing -from ...core.pydantic_utilities import IS_PYDANTIC_V2 - - -class ParameterCheck(UniversalBaseModel): - """ - Validates one argument of an expected tool call. `path` is a - dotted JSON path (e.g. `customer.email`); use zero-indexed - notation for arrays (`items.0.sku`). An empty path checks the - whole args object. - """ - - path: str = pydantic.Field() - """ - Dotted JSON path to the argument being checked. Empty means the whole args object. - """ - - mode: ParameterCheckMode - expected: typing.Optional[str] = pydantic.Field(default=None) - """ - Expected value string for `exact` and `regex` modes. - """ - - criteria: typing.Optional[str] = pydantic.Field(default=None) - """ - Natural-language criteria for `llm` mode (e.g. "is a valid email address"). - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/parameter_check_mode.py b/src/speechify/tts/types/parameter_check_mode.py deleted file mode 100644 index 27c6567..0000000 --- a/src/speechify/tts/types/parameter_check_mode.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -ParameterCheckMode = typing.Union[typing.Literal["exact", "regex", "llm"], typing.Any] diff --git a/src/speechify/tts/types/parameter_check_result.py b/src/speechify/tts/types/parameter_check_result.py deleted file mode 100644 index 7300a18..0000000 --- a/src/speechify/tts/types/parameter_check_result.py +++ /dev/null @@ -1,35 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -from .parameter_check_mode import ParameterCheckMode -import pydantic -import typing -from ...core.pydantic_utilities import IS_PYDANTIC_V2 - - -class ParameterCheckResult(UniversalBaseModel): - """ - Result of one `ParameterCheck` within a tool-call test run. - """ - - path: str - mode: ParameterCheckMode - actual_json: str = pydantic.Field() - """ - JSON-serialised actual value at `path`. - """ - - passed: bool - rationale: typing.Optional[str] = pydantic.Field(default=None) - """ - LLM rationale (populated for `llm` mode checks). - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/run_agent_tests_response.py b/src/speechify/tts/types/run_agent_tests_response.py deleted file mode 100644 index eddb061..0000000 --- a/src/speechify/tts/types/run_agent_tests_response.py +++ /dev/null @@ -1,26 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -from .agent_test_run import AgentTestRun -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import pydantic - - -class RunAgentTestsResponse(UniversalBaseModel): - """ - Response from `POST /v1/agents/{id}/tests/runs`. Contains every - newly-queued run so the client can poll each for completion. - Capped at 50 runs per call. - """ - - runs: typing.List[AgentTestRun] - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/run_batch_response.py b/src/speechify/tts/types/run_batch_response.py deleted file mode 100644 index 77d0bd6..0000000 --- a/src/speechify/tts/types/run_batch_response.py +++ /dev/null @@ -1,20 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -from .agent_test_run import AgentTestRun -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import pydantic - - -class RunBatchResponse(UniversalBaseModel): - runs: typing.List[AgentTestRun] - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/scenario_config.py b/src/speechify/tts/types/scenario_config.py deleted file mode 100644 index 9681422..0000000 --- a/src/speechify/tts/types/scenario_config.py +++ /dev/null @@ -1,68 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -import pydantic -from .simulation_message import SimulationMessage -from ...core.pydantic_utilities import IS_PYDANTIC_V2 - - -class ScenarioConfig(UniversalBaseModel): - """ - Configuration for a `scenario` test. The runner sends `context` as - a user message and asks an LLM judge to evaluate the agent response - against `success_criteria`. Optional few-shot examples sharpen the - judge's calibration. Use `initial_chat_history` to prepend prior - turns before `context`; when the history already ends with a user - message, `context` may be omitted and the agent is evaluated on - its reply to that last history turn. - """ - - context: typing.Optional[str] = pydantic.Field(default=None) - """ - User message sent to the agent to trigger the behaviour under test. Optional when `initial_chat_history` already ends with a user message. - """ - - success_criteria: str = pydantic.Field() - """ - Natural-language description of what a passing agent response looks like. - """ - - success_examples: typing.Optional[typing.List[str]] = pydantic.Field(default=None) - """ - Concrete examples of passing responses (few-shot for the judge). - """ - - failure_examples: typing.Optional[typing.List[str]] = pydantic.Field(default=None) - """ - Concrete examples of failing responses (few-shot for the judge). - """ - - initial_chat_history: typing.Optional[typing.List[SimulationMessage]] = pydantic.Field(default=None) - """ - Optional seed conversation prepended before `context`. Lets you test the agent's reply mid-conversation rather than on a cold single-turn prompt. - """ - - system_prompt_override: typing.Optional[str] = pydantic.Field(default=None) - """ - Replaces the agent's system prompt for this run only. Useful for regression-isolating prompt changes. - """ - - first_message_override: typing.Optional[str] = pydantic.Field(default=None) - """ - Replaces the agent's first message for this run only. - """ - - model_override: typing.Optional[str] = pydantic.Field(default=None) - """ - Overrides the LLM model used by the agent for this run only. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/scenario_result.py b/src/speechify/tts/types/scenario_result.py deleted file mode 100644 index c3ee1c5..0000000 --- a/src/speechify/tts/types/scenario_result.py +++ /dev/null @@ -1,42 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import pydantic -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import typing - - -class ScenarioResult(UniversalBaseModel): - """ - Result details for a `scenario` test run. - """ - - agent_response: str = pydantic.Field() - """ - The raw text response the agent produced. - """ - - passed: bool - rationale: str = pydantic.Field() - """ - LLM judge's explanation of the verdict. - """ - - score: float = pydantic.Field() - """ - 0-1 judge confidence score. - """ - - duration_ms: int = pydantic.Field() - """ - Wall-clock time for the run in milliseconds. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/search_knowledge_bases_response.py b/src/speechify/tts/types/search_knowledge_bases_response.py deleted file mode 100644 index 14d3769..0000000 --- a/src/speechify/tts/types/search_knowledge_bases_response.py +++ /dev/null @@ -1,20 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -from .knowledge_base_search_hit import KnowledgeBaseSearchHit -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import pydantic - - -class SearchKnowledgeBasesResponse(UniversalBaseModel): - hits: typing.List[KnowledgeBaseSearchHit] - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/simulation_config.py b/src/speechify/tts/types/simulation_config.py deleted file mode 100644 index d2a7b38..0000000 --- a/src/speechify/tts/types/simulation_config.py +++ /dev/null @@ -1,57 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import pydantic -import typing -from .simulation_message import SimulationMessage -from ...core.pydantic_utilities import IS_PYDANTIC_V2 - - -class SimulationConfig(UniversalBaseModel): - """ - Configuration for a `simulation` test. An AI caller drives a - multi-turn conversation with the agent according to `scenario`. - After `max_turns` exchanges (or when the agent ends the call), an - LLM judge evaluates whether `success_condition` was met. - Use `initial_chat_history` to seed the conversation at a specific - mid-flow state. - """ - - scenario: str = pydantic.Field() - """ - Instructions for the AI caller describing who they are and what they want. - """ - - success_condition: str = pydantic.Field() - """ - Natural-language description of what a passing conversation looks like. - """ - - max_turns: int = pydantic.Field() - """ - Maximum agent turns before the simulation is cut off and judged. - """ - - initial_chat_history: typing.Optional[typing.List[SimulationMessage]] = pydantic.Field(default=None) - """ - Optional seed conversation that precedes the AI caller's first generated message. - """ - - system_prompt_override: typing.Optional[str] = pydantic.Field(default=None) - """ - Replaces the agent's system prompt for this run only. - """ - - model_override: typing.Optional[str] = pydantic.Field(default=None) - """ - Overrides the LLM model used by the agent for this run only. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/simulation_message.py b/src/speechify/tts/types/simulation_message.py deleted file mode 100644 index ffb0c0e..0000000 --- a/src/speechify/tts/types/simulation_message.py +++ /dev/null @@ -1,25 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -from .simulation_message_role import SimulationMessageRole -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import typing -import pydantic - - -class SimulationMessage(UniversalBaseModel): - """ - One turn in a simulation conversation. `role` is `user` (the AI caller) or `assistant` (the agent). - """ - - role: SimulationMessageRole - content: str - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/simulation_message_role.py b/src/speechify/tts/types/simulation_message_role.py deleted file mode 100644 index 106ec8f..0000000 --- a/src/speechify/tts/types/simulation_message_role.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -SimulationMessageRole = typing.Union[typing.Literal["user", "assistant"], typing.Any] diff --git a/src/speechify/tts/types/simulation_result.py b/src/speechify/tts/types/simulation_result.py deleted file mode 100644 index a6bfb42..0000000 --- a/src/speechify/tts/types/simulation_result.py +++ /dev/null @@ -1,46 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -from .simulation_message import SimulationMessage -import pydantic -from .simulation_tool_call import SimulationToolCall -from ...core.pydantic_utilities import IS_PYDANTIC_V2 - - -class SimulationResult(UniversalBaseModel): - """ - Result details for a `simulation` test run. - """ - - transcript: typing.List[SimulationMessage] = pydantic.Field() - """ - Full synthetic conversation in order. - """ - - tool_calls: typing.Optional[typing.List[SimulationToolCall]] = pydantic.Field(default=None) - """ - Every tool invocation across all turns. - """ - - turns_used: int = pydantic.Field() - """ - Number of agent turns that ran before the simulation ended. - """ - - passed: bool - rationale: str = pydantic.Field() - """ - LLM judge's explanation of the verdict. - """ - - duration_ms: int - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/simulation_tool_call.py b/src/speechify/tts/types/simulation_tool_call.py deleted file mode 100644 index a2781af..0000000 --- a/src/speechify/tts/types/simulation_tool_call.py +++ /dev/null @@ -1,38 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import pydantic -import typing -from ...core.pydantic_utilities import IS_PYDANTIC_V2 - - -class SimulationToolCall(UniversalBaseModel): - """ - One tool invocation that occurred during a simulation run. - `mocked` is true when the call was intercepted by the run's - mock config; false when the real tool was called or when the - tool is a system tool. - """ - - turn_index: int = pydantic.Field() - """ - Zero-based index of the conversation turn in which this call occurred. - """ - - tool_name: str - args: typing.Optional[typing.Any] = None - response: typing.Optional[typing.Optional[typing.Any]] = pydantic.Field(default=None) - """ - Response returned to the agent (absent for system tools that end the call). - """ - - mocked: bool - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/system_tool_config.py b/src/speechify/tts/types/system_tool_config.py deleted file mode 100644 index a371652..0000000 --- a/src/speechify/tts/types/system_tool_config.py +++ /dev/null @@ -1,30 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -from .system_tool_config_builtin import SystemToolConfigBuiltin -import typing -from .tool_param import ToolParam -import pydantic -from ...core.pydantic_utilities import IS_PYDANTIC_V2 - - -class SystemToolConfig(UniversalBaseModel): - """ - Config shape for `kind=system`. - """ - - builtin: SystemToolConfigBuiltin - params: typing.Optional[typing.List[ToolParam]] = None - builtin_config: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = pydantic.Field(default=None) - """ - Per-builtin extras (e.g. allowed_numbers for transfer_to_number). - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/system_tool_config_builtin.py b/src/speechify/tts/types/system_tool_config_builtin.py deleted file mode 100644 index 9c7a630..0000000 --- a/src/speechify/tts/types/system_tool_config_builtin.py +++ /dev/null @@ -1,8 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -SystemToolConfigBuiltin = typing.Union[ - typing.Literal["end_call", "transfer_to_number", "transfer_to_agent", "play_keypad_touch_tone", "skip_turn"], - typing.Any, -] diff --git a/src/speechify/tts/types/system_variable_doc.py b/src/speechify/tts/types/system_variable_doc.py deleted file mode 100644 index f3a8627..0000000 --- a/src/speechify/tts/types/system_variable_doc.py +++ /dev/null @@ -1,33 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import pydantic -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import typing - - -class SystemVariableDoc(UniversalBaseModel): - """ - Documents one reserved `system__*` variable that the platform - auto-populates at session start. Customers cannot define or - override these keys. - """ - - key: str = pydantic.Field() - """ - The reserved variable key (always starts with `system__`). - """ - - description: str = pydantic.Field() - """ - What the variable contains and when it is populated. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/tenant.py b/src/speechify/tts/types/tenant.py deleted file mode 100644 index b1f1263..0000000 --- a/src/speechify/tts/types/tenant.py +++ /dev/null @@ -1,57 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import pydantic -from .tenant_plan import TenantPlan -from .tenant_data_region import TenantDataRegion -import datetime as dt -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import typing - - -class Tenant(UniversalBaseModel): - """ - A workspace the caller belongs to. - """ - - id: str = pydantic.Field() - """ - Opaque workspace ID. - """ - - name: str = pydantic.Field() - """ - Display name set by the workspace owner. - """ - - plan: TenantPlan = pydantic.Field() - """ - Billing plan tier. - """ - - data_region: TenantDataRegion = pydantic.Field() - """ - Geographic region the workspace's data is pinned to. - """ - - hipaa_mode: bool = pydantic.Field() - """ - When true, HIPAA-compliant retention and logging is enforced. - """ - - zero_retention: bool = pydantic.Field() - """ - When true, no transcript / audio payloads are retained server-side. - """ - - created_at: dt.datetime - updated_at: dt.datetime - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/tenant_data_region.py b/src/speechify/tts/types/tenant_data_region.py deleted file mode 100644 index e33656f..0000000 --- a/src/speechify/tts/types/tenant_data_region.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -TenantDataRegion = typing.Union[typing.Literal["us", "eu", "in"], typing.Any] diff --git a/src/speechify/tts/types/tenant_plan.py b/src/speechify/tts/types/tenant_plan.py deleted file mode 100644 index e3f98f9..0000000 --- a/src/speechify/tts/types/tenant_plan.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -TenantPlan = typing.Union[typing.Literal["free", "pro", "business", "enterprise"], typing.Any] diff --git a/src/speechify/tts/types/tenants_list_response.py b/src/speechify/tts/types/tenants_list_response.py deleted file mode 100644 index e4b7373..0000000 --- a/src/speechify/tts/types/tenants_list_response.py +++ /dev/null @@ -1,20 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -from .tenant import Tenant -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import pydantic - - -class TenantsListResponse(UniversalBaseModel): - tenants: typing.List[Tenant] - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/test_run_result.py b/src/speechify/tts/types/test_run_result.py deleted file mode 100644 index 4bfdf28..0000000 --- a/src/speechify/tts/types/test_run_result.py +++ /dev/null @@ -1,39 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -from .test_type import TestType -import pydantic -import typing -from .scenario_result import ScenarioResult -from .tool_call_result import ToolCallResult -from .simulation_result import SimulationResult -from ...core.pydantic_utilities import IS_PYDANTIC_V2 - - -class TestRunResult(UniversalBaseModel): - """ - Union-like result of a completed test run. Exactly one of - `scenario`, `tool_call`, or `simulation` is populated, matching - the `test_type`. - """ - - test_type: TestType - passed: bool - rationale: str = pydantic.Field() - """ - Top-level verdict explanation duplicated from the inner result for quick rendering. - """ - - duration_ms: int - scenario: typing.Optional[ScenarioResult] = None - tool_call: typing.Optional[ToolCallResult] = None - simulation: typing.Optional[SimulationResult] = None - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/test_run_status.py b/src/speechify/tts/types/test_run_status.py deleted file mode 100644 index f9924f7..0000000 --- a/src/speechify/tts/types/test_run_status.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -TestRunStatus = typing.Union[typing.Literal["queued", "running", "passed", "failed", "error"], typing.Any] diff --git a/src/speechify/tts/types/test_stats.py b/src/speechify/tts/types/test_stats.py deleted file mode 100644 index 8a946a6..0000000 --- a/src/speechify/tts/types/test_stats.py +++ /dev/null @@ -1,34 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -from .test_stats_bucket import TestStatsBucket -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import pydantic - - -class TestStats(UniversalBaseModel): - """ - Aggregate run metrics over the requested window. `buckets` is - dense - one entry per day in the window, zero-filled, so a chart - never has gaps. `by_type` counts runs per test type across the - whole window. - """ - - window_days: int - buckets: typing.List[TestStatsBucket] - total_runs: int - passed_runs: int - failed_runs: int - errored_runs: int - avg_duration_ms: int - by_type: typing.Optional[typing.Dict[str, int]] = None - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/test_stats_bucket.py b/src/speechify/tts/types/test_stats_bucket.py deleted file mode 100644 index d6d7c26..0000000 --- a/src/speechify/tts/types/test_stats_bucket.py +++ /dev/null @@ -1,30 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import pydantic -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import typing - - -class TestStatsBucket(UniversalBaseModel): - """ - One daily point on the aggregate pass-rate chart. - """ - - day: str = pydantic.Field() - """ - ISO date (YYYY-MM-DD). - """ - - passed: int - failed: int - errored: int - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/test_type.py b/src/speechify/tts/types/test_type.py deleted file mode 100644 index 5afbc08..0000000 --- a/src/speechify/tts/types/test_type.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -TestType = typing.Union[typing.Literal["scenario", "tool", "simulation"], typing.Any] diff --git a/src/speechify/tts/types/tool.py b/src/speechify/tts/types/tool.py deleted file mode 100644 index 53a7563..0000000 --- a/src/speechify/tts/types/tool.py +++ /dev/null @@ -1,39 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -from .tool_kind import ToolKind -from .tool_config import ToolConfig -import pydantic -import typing -import datetime as dt -from ...core.pydantic_utilities import IS_PYDANTIC_V2 - - -class Tool(UniversalBaseModel): - id: str - name: str - description: str - kind: ToolKind - config: ToolConfig = pydantic.Field() - """ - One of `SystemToolConfig`, `WebhookToolConfig`, or `ClientToolConfig` depending on `kind`. - """ - - webhook_secret: typing.Optional[str] = pydantic.Field(default=None) - """ - HMAC signing secret for `kind=webhook`. Returned in full **only** on the create - response; all subsequent reads return a masked placeholder. Store it on first - create — there is no way to retrieve it later. - """ - - created_at: dt.datetime - updated_at: dt.datetime - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/tool_call_config.py b/src/speechify/tts/types/tool_call_config.py deleted file mode 100644 index 8efecf8..0000000 --- a/src/speechify/tts/types/tool_call_config.py +++ /dev/null @@ -1,57 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -import pydantic -from .parameter_check import ParameterCheck -from .simulation_message import SimulationMessage -from ...core.pydantic_utilities import IS_PYDANTIC_V2 - - -class ToolCallConfig(UniversalBaseModel): - """ - Configuration for a `tool` test. The runner sends `context` as a - user message and asserts that the agent calls `expected_tool` with - arguments matching all `parameter_checks`. Use - `initial_chat_history` to test tool invocations that only make - sense mid-conversation. - """ - - context: typing.Optional[str] = pydantic.Field(default=None) - """ - User message that should cause the agent to invoke the expected tool. Optional when `initial_chat_history` already ends with a user message. - """ - - expected_tool: str = pydantic.Field() - """ - Name of the tool the agent is expected to call. - """ - - parameter_checks: typing.Optional[typing.List[ParameterCheck]] = pydantic.Field(default=None) - """ - Assertions on specific arguments of the tool call. - """ - - initial_chat_history: typing.Optional[typing.List[SimulationMessage]] = pydantic.Field(default=None) - """ - Optional seed conversation prepended before `context`. - """ - - system_prompt_override: typing.Optional[str] = pydantic.Field(default=None) - """ - Replaces the agent's system prompt for this run only. - """ - - model_override: typing.Optional[str] = pydantic.Field(default=None) - """ - Overrides the LLM model used by the agent for this run only. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/tool_call_result.py b/src/speechify/tts/types/tool_call_result.py deleted file mode 100644 index 7e9dc1c..0000000 --- a/src/speechify/tts/types/tool_call_result.py +++ /dev/null @@ -1,51 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import pydantic -import typing -from .parameter_check_result import ParameterCheckResult -from ...core.pydantic_utilities import IS_PYDANTIC_V2 - - -class ToolCallResult(UniversalBaseModel): - """ - Result details for a `tool` test run. - """ - - tool_called: str = pydantic.Field() - """ - Name of the tool the agent actually called (may differ from `expected_tool`). - """ - - tool_args: typing.Optional[typing.Optional[typing.Any]] = pydantic.Field(default=None) - """ - Arguments the agent passed to the tool, as a JSON object. - """ - - expected_tool: str = pydantic.Field() - """ - Name of the tool the test expected the agent to call. - """ - - tool_matched: bool = pydantic.Field() - """ - True when `tool_called` equals `expected_tool`. - """ - - parameter_results: typing.List[ParameterCheckResult] - passed: bool - rationale: str = pydantic.Field() - """ - Explanation of the overall verdict. - """ - - duration_ms: int - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/tool_config.py b/src/speechify/tts/types/tool_config.py deleted file mode 100644 index ee8576c..0000000 --- a/src/speechify/tts/types/tool_config.py +++ /dev/null @@ -1,8 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from .system_tool_config import SystemToolConfig -from .webhook_tool_config import WebhookToolConfig -from .client_tool_config import ClientToolConfig - -ToolConfig = typing.Union[SystemToolConfig, WebhookToolConfig, ClientToolConfig] diff --git a/src/speechify/tts/types/tool_kind.py b/src/speechify/tts/types/tool_kind.py deleted file mode 100644 index da3d6ba..0000000 --- a/src/speechify/tts/types/tool_kind.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -ToolKind = typing.Union[typing.Literal["system", "webhook", "client"], typing.Any] diff --git a/src/speechify/tts/types/tool_mock.py b/src/speechify/tts/types/tool_mock.py deleted file mode 100644 index 6fe48ca..0000000 --- a/src/speechify/tts/types/tool_mock.py +++ /dev/null @@ -1,38 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import pydantic -import typing -from ...core.pydantic_utilities import IS_PYDANTIC_V2 - - -class ToolMock(UniversalBaseModel): - """ - A canned response returned when the agent calls `tool_name`. If - `args_match` is set the mock only triggers when its value appears - as a substring of the JSON-serialised call arguments (a deliberately - simple v1 matcher — full expression support is planned). A mock - without `args_match` always matches for its tool. - """ - - tool_name: str = pydantic.Field() - """ - Name of the tool to intercept. - """ - - args_match: typing.Optional[str] = pydantic.Field(default=None) - """ - Optional substring of the JSON-serialised call arguments. When - absent the mock matches unconditionally for this tool. - """ - - response: typing.Optional[typing.Any] = None - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/tool_mock_config.py b/src/speechify/tts/types/tool_mock_config.py deleted file mode 100644 index 15d5c5e..0000000 --- a/src/speechify/tts/types/tool_mock_config.py +++ /dev/null @@ -1,32 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -from .mocking_strategy import MockingStrategy -import typing -from .tool_mock import ToolMock -import pydantic -from .no_match_behavior import NoMatchBehavior -from ...core.pydantic_utilities import IS_PYDANTIC_V2 - - -class ToolMockConfig(UniversalBaseModel): - """ - Controls tool-call interception during a test run. - """ - - strategy: MockingStrategy - mocks: typing.Optional[typing.List[ToolMock]] = pydantic.Field(default=None) - """ - Canned responses for specific tools (order matters - first match wins). - """ - - no_match_behavior: NoMatchBehavior - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/tool_param.py b/src/speechify/tts/types/tool_param.py deleted file mode 100644 index e5b253f..0000000 --- a/src/speechify/tts/types/tool_param.py +++ /dev/null @@ -1,28 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -from .tool_param_type import ToolParamType -import typing -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import pydantic - - -class ToolParam(UniversalBaseModel): - """ - One argument the LLM can pass when calling the tool. Mirrors the JSON-Schema subset standard function-calling schemas support. - """ - - name: str - type: ToolParamType - description: str - required: bool - enum: typing.Optional[typing.List[str]] = None - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/tool_param_type.py b/src/speechify/tts/types/tool_param_type.py deleted file mode 100644 index 9dc5af6..0000000 --- a/src/speechify/tts/types/tool_param_type.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -ToolParamType = typing.Union[typing.Literal["string", "number", "integer", "boolean"], typing.Any] diff --git a/src/speechify/tts/types/webhook_tool_config.py b/src/speechify/tts/types/webhook_tool_config.py deleted file mode 100644 index 468e671..0000000 --- a/src/speechify/tts/types/webhook_tool_config.py +++ /dev/null @@ -1,37 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.pydantic_utilities import UniversalBaseModel -import typing -from .webhook_tool_config_method import WebhookToolConfigMethod -import pydantic -from .tool_param import ToolParam -from ...core.pydantic_utilities import IS_PYDANTIC_V2 - - -class WebhookToolConfig(UniversalBaseModel): - """ - Config shape for `kind=webhook`. - """ - - url: str - method: typing.Optional[WebhookToolConfigMethod] = None - headers: typing.Optional[typing.Dict[str, str]] = pydantic.Field(default=None) - """ - Static headers sent with every call. `Authorization` and `X-Speechify-Signature` are reserved. - """ - - timeout_ms: typing.Optional[int] = pydantic.Field(default=None) - """ - Per-call timeout in milliseconds. - """ - - params: typing.Optional[typing.List[ToolParam]] = None - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/webhook_tool_config_method.py b/src/speechify/tts/types/webhook_tool_config_method.py deleted file mode 100644 index f8968f7..0000000 --- a/src/speechify/tts/types/webhook_tool_config_method.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -WebhookToolConfigMethod = typing.Union[typing.Literal["POST", "GET"], typing.Any] diff --git a/src/speechify/tts/voices/__init__.py b/src/speechify/tts/voices/__init__.py deleted file mode 100644 index 70e63cf..0000000 --- a/src/speechify/tts/voices/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .types import CreateVoicesRequestGender - -__all__ = ["CreateVoicesRequestGender"] diff --git a/src/speechify/tts/voices/client.py b/src/speechify/tts/voices/client.py deleted file mode 100644 index 8eb26e2..0000000 --- a/src/speechify/tts/voices/client.py +++ /dev/null @@ -1,802 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from ...core.client_wrapper import SyncClientWrapper -from ...core.request_options import RequestOptions -from ..types.get_voice import GetVoice -from ...core.pydantic_utilities import parse_obj_as -from ..errors.unauthorized_error import UnauthorizedError -from ..errors.not_found_error import NotFoundError -from ..errors.internal_server_error import InternalServerError -from json.decoder import JSONDecodeError -from ...core.api_error import ApiError -from .types.create_voices_request_gender import CreateVoicesRequestGender -from ... import core -from ..types.created_voice import CreatedVoice -from ..errors.bad_request_error import BadRequestError -from ..errors.payment_required_error import PaymentRequiredError -from ...core.jsonable_encoder import jsonable_encoder -from ...core.client_wrapper import AsyncClientWrapper - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class VoicesClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> typing.List[GetVoice]: - """ - Gets the list of voices available for the user - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - typing.List[GetVoice] - A list of voices - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.voices.list() - """ - _response = self._client_wrapper.httpx_client.request( - "v1/voices", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - typing.List[GetVoice], - parse_obj_as( - type_=typing.List[GetVoice], # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 500: - raise InternalServerError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def create( - self, - *, - name: str, - gender: CreateVoicesRequestGender, - sample: core.File, - consent: str, - locale: typing.Optional[str] = OMIT, - avatar: typing.Optional[core.File] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> CreatedVoice: - """ - Create a personal (cloned) voice for the user - - Parameters - ---------- - name : str - Name of the personal voice - - gender : CreateVoicesRequestGender - Gender marker for the personal voice - male GenderMale - female GenderFemale - notSpecified GenderNotSpecified - - sample : core.File - See core.File for more documentation - - consent : str - A **string** representing the user consent information in JSON format - This should include the fullName and email of the consenting individual. - For example, `{"fullName": "John Doe", "email": "john@example.com"}` - - locale : typing.Optional[str] - Native language (locale) of the personal voice (e.g. en-US, es-ES, etc.) - - avatar : typing.Optional[core.File] - See core.File for more documentation - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - CreatedVoice - A created voice - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.voices.create( - name="name", - gender="male", - consent="consent", - ) - """ - _response = self._client_wrapper.httpx_client.request( - "v1/voices", - method="POST", - data={ - "name": name, - "locale": locale, - "gender": gender, - "consent": consent, - }, - files={ - "sample": sample, - "avatar": avatar, - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - CreatedVoice, - parse_obj_as( - type_=CreatedVoice, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 402: - raise PaymentRequiredError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 500: - raise InternalServerError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def delete(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: - """ - Delete a personal (cloned) voice - - Parameters - ---------- - id : str - The ID of the voice to delete - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.voices.delete( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/voices/{jsonable_encoder(id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 500: - raise InternalServerError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def download_sample( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> typing.Iterator[bytes]: - """ - Download a personal (cloned) voice sample - - Parameters - ---------- - id : str - The ID of the voice to download sample for - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. You can pass in configuration such as `chunk_size`, and more to customize the request and response. - - Yields - ------ - typing.Iterator[bytes] - Voice sample audio file - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.voices.download_sample( - id="id", - ) - """ - with self._client_wrapper.httpx_client.stream( - f"v1/voices/{jsonable_encoder(id)}/sample", - method="GET", - request_options=request_options, - ) as _response: - try: - if 200 <= _response.status_code < 300: - _chunk_size = request_options.get("chunk_size", None) if request_options is not None else None - for _chunk in _response.iter_bytes(chunk_size=_chunk_size): - yield _chunk - return - _response.read() - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 500: - raise InternalServerError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - -class AsyncVoicesClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> typing.List[GetVoice]: - """ - Gets the list of voices available for the user - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - typing.List[GetVoice] - A list of voices - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.voices.list() - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/voices", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - typing.List[GetVoice], - parse_obj_as( - type_=typing.List[GetVoice], # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 500: - raise InternalServerError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def create( - self, - *, - name: str, - gender: CreateVoicesRequestGender, - sample: core.File, - consent: str, - locale: typing.Optional[str] = OMIT, - avatar: typing.Optional[core.File] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> CreatedVoice: - """ - Create a personal (cloned) voice for the user - - Parameters - ---------- - name : str - Name of the personal voice - - gender : CreateVoicesRequestGender - Gender marker for the personal voice - male GenderMale - female GenderFemale - notSpecified GenderNotSpecified - - sample : core.File - See core.File for more documentation - - consent : str - A **string** representing the user consent information in JSON format - This should include the fullName and email of the consenting individual. - For example, `{"fullName": "John Doe", "email": "john@example.com"}` - - locale : typing.Optional[str] - Native language (locale) of the personal voice (e.g. en-US, es-ES, etc.) - - avatar : typing.Optional[core.File] - See core.File for more documentation - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - CreatedVoice - A created voice - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.voices.create( - name="name", - gender="male", - consent="consent", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/voices", - method="POST", - data={ - "name": name, - "locale": locale, - "gender": gender, - "consent": consent, - }, - files={ - "sample": sample, - "avatar": avatar, - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - CreatedVoice, - parse_obj_as( - type_=CreatedVoice, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 402: - raise PaymentRequiredError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 500: - raise InternalServerError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def delete(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: - """ - Delete a personal (cloned) voice - - Parameters - ---------- - id : str - The ID of the voice to delete - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.voices.delete( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/voices/{jsonable_encoder(id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 500: - raise InternalServerError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def download_sample( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> typing.AsyncIterator[bytes]: - """ - Download a personal (cloned) voice sample - - Parameters - ---------- - id : str - The ID of the voice to download sample for - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. You can pass in configuration such as `chunk_size`, and more to customize the request and response. - - Yields - ------ - typing.AsyncIterator[bytes] - Voice sample audio file - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.voices.download_sample( - id="id", - ) - - - asyncio.run(main()) - """ - async with self._client_wrapper.httpx_client.stream( - f"v1/voices/{jsonable_encoder(id)}/sample", - method="GET", - request_options=request_options, - ) as _response: - try: - if 200 <= _response.status_code < 300: - _chunk_size = request_options.get("chunk_size", None) if request_options is not None else None - async for _chunk in _response.aiter_bytes(chunk_size=_chunk_size): - yield _chunk - return - await _response.aread() - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 500: - raise InternalServerError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/src/speechify/tts/voices/types/__init__.py b/src/speechify/tts/voices/types/__init__.py deleted file mode 100644 index 6704596..0000000 --- a/src/speechify/tts/voices/types/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .create_voices_request_gender import CreateVoicesRequestGender - -__all__ = ["CreateVoicesRequestGender"] diff --git a/src/speechify/tts/workspaces/__init__.py b/src/speechify/tts/workspaces/__init__.py deleted file mode 100644 index f3ea265..0000000 --- a/src/speechify/tts/workspaces/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - diff --git a/src/speechify/tts/workspaces/client.py b/src/speechify/tts/workspaces/client.py deleted file mode 100644 index ec1fa67..0000000 --- a/src/speechify/tts/workspaces/client.py +++ /dev/null @@ -1,2186 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from ...core.client_wrapper import SyncClientWrapper -from ...core.request_options import RequestOptions -from ..types.tenants_list_response import TenantsListResponse -from ...core.pydantic_utilities import parse_obj_as -from ..errors.unauthorized_error import UnauthorizedError -from json.decoder import JSONDecodeError -from ...core.api_error import ApiError -from ..types.tenant import Tenant -from ..errors.bad_request_error import BadRequestError -from ..errors.not_found_error import NotFoundError -from ..types.members_list_response import MembersListResponse -from ..errors.conflict_error import ConflictError -from ...core.jsonable_encoder import jsonable_encoder -from ..errors.forbidden_error import ForbiddenError -from ..types.member_role import MemberRole -from ..types.member import Member -from ..types.invites_list_response import InvitesListResponse -from ..types.invite import Invite -from ..types.invite_preview import InvitePreview -from ...core.client_wrapper import AsyncClientWrapper - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class WorkspacesClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> TenantsListResponse: - """ - List every workspace the authenticated user belongs to. Powers the workspace switcher. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - TenantsListResponse - Workspaces for the authenticated user. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.workspaces.list() - """ - _response = self._client_wrapper.httpx_client.request( - "v1/tenants", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - TenantsListResponse, - parse_obj_as( - type_=TenantsListResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def create( - self, *, name: typing.Optional[str] = OMIT, request_options: typing.Optional[RequestOptions] = None - ) -> Tenant: - """ - Create a new workspace with the authenticated user as owner. - The caller must switch their active workspace client-side via - the `X-Tenant-ID` header to act on the new tenant. - - Parameters - ---------- - name : typing.Optional[str] - Display name for the new workspace. Trimmed; must be 120 characters or fewer. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Tenant - The newly-created workspace. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.workspaces.create() - """ - _response = self._client_wrapper.httpx_client.request( - "v1/tenants", - method="POST", - json={ - "name": name, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - Tenant, - parse_obj_as( - type_=Tenant, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def get_current(self, *, request_options: typing.Optional[RequestOptions] = None) -> Tenant: - """ - Retrieve the workspace currently selected by the caller (via `X-Tenant-ID` or auto-resolved). - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Tenant - The current workspace. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.workspaces.get_current() - """ - _response = self._client_wrapper.httpx_client.request( - "v1/tenants/current", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - Tenant, - parse_obj_as( - type_=Tenant, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def update_current(self, *, name: str, request_options: typing.Optional[RequestOptions] = None) -> Tenant: - """ - Rename the current workspace. Owner or admin only. - - Parameters - ---------- - name : str - New display name. Required; must be 120 characters or fewer. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Tenant - The updated workspace. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.workspaces.update_current( - name="name", - ) - """ - _response = self._client_wrapper.httpx_client.request( - "v1/tenants/current", - method="PATCH", - json={ - "name": name, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - Tenant, - parse_obj_as( - type_=Tenant, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def list_members(self, *, request_options: typing.Optional[RequestOptions] = None) -> MembersListResponse: - """ - List every member of the current workspace. Any member may call this. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - MembersListResponse - Members of the current workspace. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.workspaces.list_members() - """ - _response = self._client_wrapper.httpx_client.request( - "v1/tenants/current/members", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - MembersListResponse, - parse_obj_as( - type_=MembersListResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def leave(self, *, request_options: typing.Optional[RequestOptions] = None) -> None: - """ - Remove the authenticated caller from the current workspace. - Refused with 409 when the caller is the last owner — promote - another member to owner first. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.workspaces.leave() - """ - _response = self._client_wrapper.httpx_client.request( - "v1/tenants/current/members/leave", - method="POST", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 409: - raise ConflictError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def remove_member(self, user_uid: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: - """ - Remove a member from the current workspace. Owner or admin - only. The caller cannot remove themselves — use - `POST /v1/tenants/current/members/leave` instead. - - Parameters - ---------- - user_uid : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.workspaces.remove_member( - user_uid="user_uid", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/tenants/current/members/{jsonable_encoder(user_uid)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 403: - raise ForbiddenError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 409: - raise ConflictError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def update_member_role( - self, user_uid: str, *, role: MemberRole, request_options: typing.Optional[RequestOptions] = None - ) -> Member: - """ - Change a member's role. Owner only — admins may add or remove - members but may not change roles. Refused with 409 when - demoting the last remaining owner. - - Parameters - ---------- - user_uid : str - - role : MemberRole - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Member - The updated member. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.workspaces.update_member_role( - user_uid="user_uid", - role="owner", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/tenants/current/members/{jsonable_encoder(user_uid)}", - method="PATCH", - json={ - "role": role, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - Member, - parse_obj_as( - type_=Member, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 403: - raise ForbiddenError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 409: - raise ConflictError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def list_invites(self, *, request_options: typing.Optional[RequestOptions] = None) -> InvitesListResponse: - """ - List outstanding invites for the current workspace. Invite tokens are redacted. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - InvitesListResponse - Outstanding invites. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.workspaces.list_invites() - """ - _response = self._client_wrapper.httpx_client.request( - "v1/tenants/current/invites", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - InvitesListResponse, - parse_obj_as( - type_=InvitesListResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def create_invite(self, *, email: str, request_options: typing.Optional[RequestOptions] = None) -> Invite: - """ - Create an invite to the current workspace. Owner or admin only. - The response contains the invite token ONCE — subsequent list - calls redact it. - - Parameters - ---------- - email : str - Email of the person to invite. Validated as an RFC 5322 address. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Invite - The created invite (token included). - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.workspaces.create_invite( - email="email", - ) - """ - _response = self._client_wrapper.httpx_client.request( - "v1/tenants/current/invites", - method="POST", - json={ - "email": email, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - Invite, - parse_obj_as( - type_=Invite, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 403: - raise ForbiddenError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def revoke_invite(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: - """ - Revoke an outstanding invite. Owner or admin only. Idempotent. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.workspaces.revoke_invite( - id="id", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/tenants/current/invites/{jsonable_encoder(id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 403: - raise ForbiddenError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def accept_invite(self, token: str, *, request_options: typing.Optional[RequestOptions] = None) -> Tenant: - """ - Accept a workspace invite. The authenticated caller is joined - to the invite's workspace as a member. Expired, revoked, or - already-accepted tokens return 404 to avoid token enumeration. - - Parameters - ---------- - token : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Tenant - The workspace the caller just joined. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.workspaces.accept_invite( - token="token", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/invites/{jsonable_encoder(token)}/accept", - method="POST", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - Tenant, - parse_obj_as( - type_=Tenant, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def preview_invite(self, token: str, *, request_options: typing.Optional[RequestOptions] = None) -> InvitePreview: - """ - Preview a workspace invite without authenticating. Returns the - workspace name, inviter details, and expiry so the `/join/{token}` - page can render before the recipient signs in. Anyone with the - token can already accept, so this endpoint deliberately surfaces - the same information a caller would see after accepting. Invalid - tokens (unknown, expired, revoked, already-accepted, or pointing - at a soft-deleted workspace) collapse to a single 404 to prevent - enumeration. - - Parameters - ---------- - token : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - InvitePreview - Preview metadata for a valid, active invite. - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.workspaces.preview_invite( - token="token", - ) - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/invites/{jsonable_encoder(token)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - InvitePreview, - parse_obj_as( - type_=InvitePreview, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - def transfer_workspace_owner( - self, *, user_uid: str, request_options: typing.Optional[RequestOptions] = None - ) -> None: - """ - Transfer ownership of the current workspace atomically. Promotes - the target member to owner and demotes the caller to admin in a - single transaction. Owner-only; admins cannot hand off a role - they were never granted. Prefer this over two PATCH calls to - `/v1/tenants/current/members/{user_uid}`: a sole-owner caller - cannot demote themselves first without tripping the last-owner - guard, which this endpoint sidesteps by promoting before - demoting. - - Parameters - ---------- - user_uid : str - Firebase UID of the member who will become the new owner. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - from speechify import Speechify - - client = Speechify( - token="YOUR_TOKEN", - ) - client.tts.workspaces.transfer_workspace_owner( - user_uid="user_uid", - ) - """ - _response = self._client_wrapper.httpx_client.request( - "v1/tenants/current/transfer-owner", - method="POST", - json={ - "user_uid": user_uid, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 403: - raise ForbiddenError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - -class AsyncWorkspacesClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> TenantsListResponse: - """ - List every workspace the authenticated user belongs to. Powers the workspace switcher. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - TenantsListResponse - Workspaces for the authenticated user. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.workspaces.list() - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/tenants", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - TenantsListResponse, - parse_obj_as( - type_=TenantsListResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def create( - self, *, name: typing.Optional[str] = OMIT, request_options: typing.Optional[RequestOptions] = None - ) -> Tenant: - """ - Create a new workspace with the authenticated user as owner. - The caller must switch their active workspace client-side via - the `X-Tenant-ID` header to act on the new tenant. - - Parameters - ---------- - name : typing.Optional[str] - Display name for the new workspace. Trimmed; must be 120 characters or fewer. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Tenant - The newly-created workspace. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.workspaces.create() - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/tenants", - method="POST", - json={ - "name": name, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - Tenant, - parse_obj_as( - type_=Tenant, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def get_current(self, *, request_options: typing.Optional[RequestOptions] = None) -> Tenant: - """ - Retrieve the workspace currently selected by the caller (via `X-Tenant-ID` or auto-resolved). - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Tenant - The current workspace. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.workspaces.get_current() - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/tenants/current", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - Tenant, - parse_obj_as( - type_=Tenant, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def update_current(self, *, name: str, request_options: typing.Optional[RequestOptions] = None) -> Tenant: - """ - Rename the current workspace. Owner or admin only. - - Parameters - ---------- - name : str - New display name. Required; must be 120 characters or fewer. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Tenant - The updated workspace. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.workspaces.update_current( - name="name", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/tenants/current", - method="PATCH", - json={ - "name": name, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - Tenant, - parse_obj_as( - type_=Tenant, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def list_members(self, *, request_options: typing.Optional[RequestOptions] = None) -> MembersListResponse: - """ - List every member of the current workspace. Any member may call this. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - MembersListResponse - Members of the current workspace. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.workspaces.list_members() - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/tenants/current/members", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - MembersListResponse, - parse_obj_as( - type_=MembersListResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def leave(self, *, request_options: typing.Optional[RequestOptions] = None) -> None: - """ - Remove the authenticated caller from the current workspace. - Refused with 409 when the caller is the last owner — promote - another member to owner first. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.workspaces.leave() - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/tenants/current/members/leave", - method="POST", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 409: - raise ConflictError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def remove_member(self, user_uid: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: - """ - Remove a member from the current workspace. Owner or admin - only. The caller cannot remove themselves — use - `POST /v1/tenants/current/members/leave` instead. - - Parameters - ---------- - user_uid : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.workspaces.remove_member( - user_uid="user_uid", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/tenants/current/members/{jsonable_encoder(user_uid)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 403: - raise ForbiddenError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 409: - raise ConflictError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def update_member_role( - self, user_uid: str, *, role: MemberRole, request_options: typing.Optional[RequestOptions] = None - ) -> Member: - """ - Change a member's role. Owner only — admins may add or remove - members but may not change roles. Refused with 409 when - demoting the last remaining owner. - - Parameters - ---------- - user_uid : str - - role : MemberRole - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Member - The updated member. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.workspaces.update_member_role( - user_uid="user_uid", - role="owner", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/tenants/current/members/{jsonable_encoder(user_uid)}", - method="PATCH", - json={ - "role": role, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - Member, - parse_obj_as( - type_=Member, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 403: - raise ForbiddenError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 409: - raise ConflictError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def list_invites(self, *, request_options: typing.Optional[RequestOptions] = None) -> InvitesListResponse: - """ - List outstanding invites for the current workspace. Invite tokens are redacted. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - InvitesListResponse - Outstanding invites. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.workspaces.list_invites() - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/tenants/current/invites", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - InvitesListResponse, - parse_obj_as( - type_=InvitesListResponse, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def create_invite(self, *, email: str, request_options: typing.Optional[RequestOptions] = None) -> Invite: - """ - Create an invite to the current workspace. Owner or admin only. - The response contains the invite token ONCE — subsequent list - calls redact it. - - Parameters - ---------- - email : str - Email of the person to invite. Validated as an RFC 5322 address. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Invite - The created invite (token included). - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.workspaces.create_invite( - email="email", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/tenants/current/invites", - method="POST", - json={ - "email": email, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - Invite, - parse_obj_as( - type_=Invite, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 403: - raise ForbiddenError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def revoke_invite(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: - """ - Revoke an outstanding invite. Owner or admin only. Idempotent. - - Parameters - ---------- - id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.workspaces.revoke_invite( - id="id", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/tenants/current/invites/{jsonable_encoder(id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 403: - raise ForbiddenError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def accept_invite(self, token: str, *, request_options: typing.Optional[RequestOptions] = None) -> Tenant: - """ - Accept a workspace invite. The authenticated caller is joined - to the invite's workspace as a member. Expired, revoked, or - already-accepted tokens return 404 to avoid token enumeration. - - Parameters - ---------- - token : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Tenant - The workspace the caller just joined. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.workspaces.accept_invite( - token="token", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/invites/{jsonable_encoder(token)}/accept", - method="POST", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - Tenant, - parse_obj_as( - type_=Tenant, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def preview_invite( - self, token: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> InvitePreview: - """ - Preview a workspace invite without authenticating. Returns the - workspace name, inviter details, and expiry so the `/join/{token}` - page can render before the recipient signs in. Anyone with the - token can already accept, so this endpoint deliberately surfaces - the same information a caller would see after accepting. Invalid - tokens (unknown, expired, revoked, already-accepted, or pointing - at a soft-deleted workspace) collapse to a single 404 to prevent - enumeration. - - Parameters - ---------- - token : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - InvitePreview - Preview metadata for a valid, active invite. - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.workspaces.preview_invite( - token="token", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/invites/{jsonable_encoder(token)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return typing.cast( - InvitePreview, - parse_obj_as( - type_=InvitePreview, # type: ignore - object_=_response.json(), - ), - ) - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) - - async def transfer_workspace_owner( - self, *, user_uid: str, request_options: typing.Optional[RequestOptions] = None - ) -> None: - """ - Transfer ownership of the current workspace atomically. Promotes - the target member to owner and demotes the caller to admin in a - single transaction. Owner-only; admins cannot hand off a role - they were never granted. Prefer this over two PATCH calls to - `/v1/tenants/current/members/{user_uid}`: a sole-owner caller - cannot demote themselves first without tripping the last-owner - guard, which this endpoint sidesteps by promoting before - demoting. - - Parameters - ---------- - user_uid : str - Firebase UID of the member who will become the new owner. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - import asyncio - - from speechify import AsyncSpeechify - - client = AsyncSpeechify( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.tts.workspaces.transfer_workspace_owner( - user_uid="user_uid", - ) - - - asyncio.run(main()) - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/tenants/current/transfer-owner", - method="POST", - json={ - "user_uid": user_uid, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return - if _response.status_code == 400: - raise BadRequestError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 401: - raise UnauthorizedError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 403: - raise ForbiddenError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - if _response.status_code == 404: - raise NotFoundError( - typing.cast( - typing.Optional[typing.Any], - parse_obj_as( - type_=typing.Optional[typing.Any], # type: ignore - object_=_response.json(), - ), - ) - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/src/speechify/types/__init__.py b/src/speechify/types/__init__.py new file mode 100644 index 0000000..bbc6a1d --- /dev/null +++ b/src/speechify/types/__init__.py @@ -0,0 +1,98 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .create_voice_language import CreateVoiceLanguage + from .create_voice_model import CreateVoiceModel + from .create_voice_model_name import CreateVoiceModelName + from .created_voice import CreatedVoice + from .created_voice_gender import CreatedVoiceGender + from .created_voice_type import CreatedVoiceType + from .error import Error + from .error_code import ErrorCode + from .error_detail import ErrorDetail + from .get_speech_options_request import GetSpeechOptionsRequest + from .get_speech_response import GetSpeechResponse + from .get_speech_response_audio_format import GetSpeechResponseAudioFormat + from .get_stream_options_request import GetStreamOptionsRequest + from .get_voice import GetVoice + from .get_voice_gender import GetVoiceGender + from .get_voice_language import GetVoiceLanguage + from .get_voice_type import GetVoiceType + from .get_voices_model import GetVoicesModel + from .get_voices_model_name import GetVoicesModelName + from .nested_chunk import NestedChunk + from .speech_marks import SpeechMarks +_dynamic_imports: typing.Dict[str, str] = { + "CreateVoiceLanguage": ".create_voice_language", + "CreateVoiceModel": ".create_voice_model", + "CreateVoiceModelName": ".create_voice_model_name", + "CreatedVoice": ".created_voice", + "CreatedVoiceGender": ".created_voice_gender", + "CreatedVoiceType": ".created_voice_type", + "Error": ".error", + "ErrorCode": ".error_code", + "ErrorDetail": ".error_detail", + "GetSpeechOptionsRequest": ".get_speech_options_request", + "GetSpeechResponse": ".get_speech_response", + "GetSpeechResponseAudioFormat": ".get_speech_response_audio_format", + "GetStreamOptionsRequest": ".get_stream_options_request", + "GetVoice": ".get_voice", + "GetVoiceGender": ".get_voice_gender", + "GetVoiceLanguage": ".get_voice_language", + "GetVoiceType": ".get_voice_type", + "GetVoicesModel": ".get_voices_model", + "GetVoicesModelName": ".get_voices_model_name", + "NestedChunk": ".nested_chunk", + "SpeechMarks": ".speech_marks", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "CreateVoiceLanguage", + "CreateVoiceModel", + "CreateVoiceModelName", + "CreatedVoice", + "CreatedVoiceGender", + "CreatedVoiceType", + "Error", + "ErrorCode", + "ErrorDetail", + "GetSpeechOptionsRequest", + "GetSpeechResponse", + "GetSpeechResponseAudioFormat", + "GetStreamOptionsRequest", + "GetVoice", + "GetVoiceGender", + "GetVoiceLanguage", + "GetVoiceType", + "GetVoicesModel", + "GetVoicesModelName", + "NestedChunk", + "SpeechMarks", +] diff --git a/src/speechify/tts/types/create_voice_language.py b/src/speechify/types/create_voice_language.py similarity index 82% rename from src/speechify/tts/types/create_voice_language.py rename to src/speechify/types/create_voice_language.py index b60313b..bf99e97 100644 --- a/src/speechify/tts/types/create_voice_language.py +++ b/src/speechify/types/create_voice_language.py @@ -1,9 +1,9 @@ # This file was auto-generated by Fern from our API Definition. -from ...core.pydantic_utilities import UniversalBaseModel import typing -from ...core.pydantic_utilities import IS_PYDANTIC_V2 + import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel class CreateVoiceLanguage(UniversalBaseModel): diff --git a/src/speechify/tts/types/create_voice_model.py b/src/speechify/types/create_voice_model.py similarity index 85% rename from src/speechify/tts/types/create_voice_model.py rename to src/speechify/types/create_voice_model.py index 9f97b91..00be24f 100644 --- a/src/speechify/tts/types/create_voice_model.py +++ b/src/speechify/types/create_voice_model.py @@ -1,11 +1,11 @@ # This file was auto-generated by Fern from our API Definition. -from ...core.pydantic_utilities import UniversalBaseModel import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel from .create_voice_language import CreateVoiceLanguage from .create_voice_model_name import CreateVoiceModelName -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import pydantic class CreateVoiceModel(UniversalBaseModel): diff --git a/src/speechify/types/create_voice_model_name.py b/src/speechify/types/create_voice_model_name.py new file mode 100644 index 0000000..e82711f --- /dev/null +++ b/src/speechify/types/create_voice_model_name.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +CreateVoiceModelName = typing.Union[typing.Literal["simba-english", "simba-multilingual", "simba-3.0"], typing.Any] diff --git a/src/speechify/tts/types/created_voice.py b/src/speechify/types/created_voice.py similarity index 87% rename from src/speechify/tts/types/created_voice.py rename to src/speechify/types/created_voice.py index 6eba08b..6c05018 100644 --- a/src/speechify/tts/types/created_voice.py +++ b/src/speechify/types/created_voice.py @@ -1,12 +1,12 @@ # This file was auto-generated by Fern from our API Definition. -from ...core.pydantic_utilities import UniversalBaseModel import typing -from .created_voice_gender import CreatedVoiceGender + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel from .create_voice_model import CreateVoiceModel +from .created_voice_gender import CreatedVoiceGender from .created_voice_type import CreatedVoiceType -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import pydantic class CreatedVoice(UniversalBaseModel): diff --git a/src/speechify/tts/types/created_voice_gender.py b/src/speechify/types/created_voice_gender.py similarity index 100% rename from src/speechify/tts/types/created_voice_gender.py rename to src/speechify/types/created_voice_gender.py diff --git a/src/speechify/tts/types/created_voice_type.py b/src/speechify/types/created_voice_type.py similarity index 100% rename from src/speechify/tts/types/created_voice_type.py rename to src/speechify/types/created_voice_type.py diff --git a/src/speechify/types/error.py b/src/speechify/types/error.py new file mode 100644 index 0000000..851bdf8 --- /dev/null +++ b/src/speechify/types/error.py @@ -0,0 +1,38 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from .error_detail import ErrorDetail + + +class Error(UniversalBaseModel): + """ + Standard error envelope returned on every non-2xx response. + Content-Type is `application/json`. The shape mirrors OpenAI / + Anthropic / Stripe style: a machine-readable `error.code` for + SDK consumers to switch on, a human `error.message` for UI, + and an optional `error.fields` map for per-field validation + errors. `request_id` matches the `X-Request-ID` response + header and is what customers quote when filing support + tickets. + """ + + error: ErrorDetail + request_id: typing.Optional[str] = pydantic.Field(default=None) + """ + Server-side request identifier. Echoes the + `X-Request-ID` response header. Stable across the + request's lifetime, written to structured logs, and + useful when reporting issues. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/speechify/types/error_code.py b/src/speechify/types/error_code.py new file mode 100644 index 0000000..a4ce0b1 --- /dev/null +++ b/src/speechify/types/error_code.py @@ -0,0 +1,49 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +ErrorCode = typing.Union[ + typing.Literal[ + "bad_request", + "validation_failed", + "unauthorized", + "payment_required", + "forbidden", + "not_found", + "method_not_allowed", + "conflict", + "payload_too_large", + "unsupported_media_type", + "rate_limited", + "internal_error", + "upstream_failure", + "service_unavailable", + "caller_not_found", + "credential_not_found", + "agent_not_found", + "kb_not_found", + "kb_document_not_found", + "kb_folder_not_found", + "tool_not_found", + "conversation_not_found", + "phone_number_not_found", + "sip_trunk_not_found", + "voice_not_found", + "audio_asset_not_found", + "builtin_not_found", + "batch_not_found", + "agent_test_not_found", + "workspace_not_found", + "invite_not_found", + "insufficient_scope", + "purchased_numbers_not_included", + "phone_number_quota_reached", + "batch_calls_not_included", + "voice_cloning_not_included", + "workspace_last_owner", + "workspace_last_workspace", + "invite_email_mismatch", + "invite_already_pending", + ], + typing.Any, +] diff --git a/src/speechify/types/error_detail.py b/src/speechify/types/error_detail.py new file mode 100644 index 0000000..e2c1fa8 --- /dev/null +++ b/src/speechify/types/error_detail.py @@ -0,0 +1,37 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from .error_code import ErrorCode + + +class ErrorDetail(UniversalBaseModel): + code: ErrorCode + message: str = pydantic.Field() + """ + Human-readable explanation of this specific occurrence. + Safe to surface in UI banners or pass to support. The + wording can change between releases; clients should + match on `code`, not on the message string. + """ + + fields: typing.Optional[typing.Dict[str, str]] = pydantic.Field(default=None) + """ + Per-field validation errors as `path -> message`. Only + present on 400 responses caused by request validation + (typically code=`validation_failed`). Keys are field + paths in dotted/bracket notation; values are short + human explanations safe to inline-surface next to the + offending form field. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/speechify/tts/types/get_speech_options_request.py b/src/speechify/types/get_speech_options_request.py similarity index 93% rename from src/speechify/tts/types/get_speech_options_request.py rename to src/speechify/types/get_speech_options_request.py index 3e2f52e..3764e3f 100644 --- a/src/speechify/tts/types/get_speech_options_request.py +++ b/src/speechify/types/get_speech_options_request.py @@ -1,9 +1,9 @@ # This file was auto-generated by Fern from our API Definition. -from ...core.pydantic_utilities import UniversalBaseModel import typing + import pydantic -from ...core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel class GetSpeechOptionsRequest(UniversalBaseModel): diff --git a/src/speechify/tts/types/get_speech_response.py b/src/speechify/types/get_speech_response.py similarity index 89% rename from src/speechify/tts/types/get_speech_response.py rename to src/speechify/types/get_speech_response.py index 3943cdf..bf5a964 100644 --- a/src/speechify/tts/types/get_speech_response.py +++ b/src/speechify/types/get_speech_response.py @@ -1,11 +1,11 @@ # This file was auto-generated by Fern from our API Definition. -from ...core.pydantic_utilities import UniversalBaseModel +import typing + import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel from .get_speech_response_audio_format import GetSpeechResponseAudioFormat from .speech_marks import SpeechMarks -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import typing class GetSpeechResponse(UniversalBaseModel): diff --git a/src/speechify/tts/types/get_speech_response_audio_format.py b/src/speechify/types/get_speech_response_audio_format.py similarity index 100% rename from src/speechify/tts/types/get_speech_response_audio_format.py rename to src/speechify/types/get_speech_response_audio_format.py diff --git a/src/speechify/tts/types/get_stream_options_request.py b/src/speechify/types/get_stream_options_request.py similarity index 93% rename from src/speechify/tts/types/get_stream_options_request.py rename to src/speechify/types/get_stream_options_request.py index 96ece1f..f2409f7 100644 --- a/src/speechify/tts/types/get_stream_options_request.py +++ b/src/speechify/types/get_stream_options_request.py @@ -1,9 +1,9 @@ # This file was auto-generated by Fern from our API Definition. -from ...core.pydantic_utilities import UniversalBaseModel import typing + import pydantic -from ...core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel class GetStreamOptionsRequest(UniversalBaseModel): diff --git a/src/speechify/tts/types/get_voice.py b/src/speechify/types/get_voice.py similarity index 88% rename from src/speechify/tts/types/get_voice.py rename to src/speechify/types/get_voice.py index 0466b56..63e3322 100644 --- a/src/speechify/tts/types/get_voice.py +++ b/src/speechify/types/get_voice.py @@ -1,12 +1,12 @@ # This file was auto-generated by Fern from our API Definition. -from ...core.pydantic_utilities import UniversalBaseModel import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel from .get_voice_gender import GetVoiceGender -from .get_voices_model import GetVoicesModel from .get_voice_type import GetVoiceType -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import pydantic +from .get_voices_model import GetVoicesModel class GetVoice(UniversalBaseModel): diff --git a/src/speechify/tts/types/get_voice_gender.py b/src/speechify/types/get_voice_gender.py similarity index 100% rename from src/speechify/tts/types/get_voice_gender.py rename to src/speechify/types/get_voice_gender.py diff --git a/src/speechify/tts/types/get_voice_language.py b/src/speechify/types/get_voice_language.py similarity index 81% rename from src/speechify/tts/types/get_voice_language.py rename to src/speechify/types/get_voice_language.py index 3062016..cde8316 100644 --- a/src/speechify/tts/types/get_voice_language.py +++ b/src/speechify/types/get_voice_language.py @@ -1,9 +1,9 @@ # This file was auto-generated by Fern from our API Definition. -from ...core.pydantic_utilities import UniversalBaseModel import typing -from ...core.pydantic_utilities import IS_PYDANTIC_V2 + import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel class GetVoiceLanguage(UniversalBaseModel): diff --git a/src/speechify/tts/types/get_voice_type.py b/src/speechify/types/get_voice_type.py similarity index 100% rename from src/speechify/tts/types/get_voice_type.py rename to src/speechify/types/get_voice_type.py diff --git a/src/speechify/tts/types/get_voices_model.py b/src/speechify/types/get_voices_model.py similarity index 84% rename from src/speechify/tts/types/get_voices_model.py rename to src/speechify/types/get_voices_model.py index d68df75..57862c6 100644 --- a/src/speechify/tts/types/get_voices_model.py +++ b/src/speechify/types/get_voices_model.py @@ -1,11 +1,11 @@ # This file was auto-generated by Fern from our API Definition. -from ...core.pydantic_utilities import UniversalBaseModel import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel from .get_voice_language import GetVoiceLanguage from .get_voices_model_name import GetVoicesModelName -from ...core.pydantic_utilities import IS_PYDANTIC_V2 -import pydantic class GetVoicesModel(UniversalBaseModel): diff --git a/src/speechify/types/get_voices_model_name.py b/src/speechify/types/get_voices_model_name.py new file mode 100644 index 0000000..7ec7d98 --- /dev/null +++ b/src/speechify/types/get_voices_model_name.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +GetVoicesModelName = typing.Union[typing.Literal["simba-english", "simba-multilingual", "simba-3.0"], typing.Any] diff --git a/src/speechify/tts/types/nested_chunk.py b/src/speechify/types/nested_chunk.py similarity index 88% rename from src/speechify/tts/types/nested_chunk.py rename to src/speechify/types/nested_chunk.py index d842e83..e725496 100644 --- a/src/speechify/tts/types/nested_chunk.py +++ b/src/speechify/types/nested_chunk.py @@ -1,9 +1,9 @@ # This file was auto-generated by Fern from our API Definition. -from ...core.pydantic_utilities import UniversalBaseModel import typing -from ...core.pydantic_utilities import IS_PYDANTIC_V2 + import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel class NestedChunk(UniversalBaseModel): diff --git a/src/speechify/tts/types/speech_marks.py b/src/speechify/types/speech_marks.py similarity index 88% rename from src/speechify/tts/types/speech_marks.py rename to src/speechify/types/speech_marks.py index d0bc925..4db6f1b 100644 --- a/src/speechify/tts/types/speech_marks.py +++ b/src/speechify/types/speech_marks.py @@ -1,10 +1,10 @@ # This file was auto-generated by Fern from our API Definition. -from ...core.pydantic_utilities import UniversalBaseModel import typing -from .nested_chunk import NestedChunk + import pydantic -from ...core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from .nested_chunk import NestedChunk class SpeechMarks(UniversalBaseModel): diff --git a/src/speechify/voices/__init__.py b/src/speechify/voices/__init__.py new file mode 100644 index 0000000..ac3d80d --- /dev/null +++ b/src/speechify/voices/__init__.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import CreateVoicesRequestGender +_dynamic_imports: typing.Dict[str, str] = {"CreateVoicesRequestGender": ".types"} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["CreateVoicesRequestGender"] diff --git a/src/speechify/voices/client.py b/src/speechify/voices/client.py new file mode 100644 index 0000000..1ddb3fa --- /dev/null +++ b/src/speechify/voices/client.py @@ -0,0 +1,399 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .. import core +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from ..types.created_voice import CreatedVoice +from ..types.get_voice import GetVoice +from .raw_client import AsyncRawVoicesClient, RawVoicesClient +from .types.create_voices_request_gender import CreateVoicesRequestGender + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class VoicesClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawVoicesClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawVoicesClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawVoicesClient + """ + return self._raw_client + + def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> typing.List[GetVoice]: + """ + Gets the list of voices available for the user + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + typing.List[GetVoice] + A list of voices + + Examples + -------- + from speechify import Speechify + + client = Speechify( + api_key="YOUR_API_KEY", + ) + client.voices.list() + """ + _response = self._raw_client.list(request_options=request_options) + return _response.data + + def create( + self, + *, + name: str, + gender: CreateVoicesRequestGender, + sample: core.File, + consent: str, + locale: typing.Optional[str] = OMIT, + avatar: typing.Optional[core.File] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> CreatedVoice: + """ + Create a personal (cloned) voice for the user + + Parameters + ---------- + name : str + Name of the personal voice + + gender : CreateVoicesRequestGender + Gender marker for the personal voice + male GenderMale + female GenderFemale + notSpecified GenderNotSpecified + + sample : core.File + See core.File for more documentation + + consent : str + A **string** representing the user consent information in JSON format + This should include the fullName and email of the consenting individual. + For example, `{"fullName": "John Doe", "email": "john@example.com"}` + + locale : typing.Optional[str] + Native language (locale) of the personal voice (e.g. en-US, es-ES, etc.) + + avatar : typing.Optional[core.File] + See core.File for more documentation + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + CreatedVoice + A created voice + + Examples + -------- + from speechify import Speechify + + client = Speechify( + api_key="YOUR_API_KEY", + ) + client.voices.create( + name="name", + gender="male", + consent="consent", + ) + """ + _response = self._raw_client.create( + name=name, + gender=gender, + sample=sample, + consent=consent, + locale=locale, + avatar=avatar, + request_options=request_options, + ) + return _response.data + + def delete(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: + """ + Delete a personal (cloned) voice + + Parameters + ---------- + id : str + The ID of the voice to delete + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + from speechify import Speechify + + client = Speechify( + api_key="YOUR_API_KEY", + ) + client.voices.delete( + id="id", + ) + """ + _response = self._raw_client.delete(id, request_options=request_options) + return _response.data + + def download_sample( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> typing.Iterator[bytes]: + """ + Download a personal (cloned) voice sample + + Parameters + ---------- + id : str + The ID of the voice to download sample for + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. You can pass in configuration such as `chunk_size`, and more to customize the request and response. + + Returns + ------- + typing.Iterator[bytes] + Voice sample audio file + + Examples + -------- + from speechify import Speechify + + client = Speechify( + api_key="YOUR_API_KEY", + ) + client.voices.download_sample( + id="id", + ) + """ + with self._raw_client.download_sample(id, request_options=request_options) as r: + yield from r.data + + +class AsyncVoicesClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawVoicesClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawVoicesClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawVoicesClient + """ + return self._raw_client + + async def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> typing.List[GetVoice]: + """ + Gets the list of voices available for the user + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + typing.List[GetVoice] + A list of voices + + Examples + -------- + import asyncio + + from speechify import AsyncSpeechify + + client = AsyncSpeechify( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.voices.list() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list(request_options=request_options) + return _response.data + + async def create( + self, + *, + name: str, + gender: CreateVoicesRequestGender, + sample: core.File, + consent: str, + locale: typing.Optional[str] = OMIT, + avatar: typing.Optional[core.File] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> CreatedVoice: + """ + Create a personal (cloned) voice for the user + + Parameters + ---------- + name : str + Name of the personal voice + + gender : CreateVoicesRequestGender + Gender marker for the personal voice + male GenderMale + female GenderFemale + notSpecified GenderNotSpecified + + sample : core.File + See core.File for more documentation + + consent : str + A **string** representing the user consent information in JSON format + This should include the fullName and email of the consenting individual. + For example, `{"fullName": "John Doe", "email": "john@example.com"}` + + locale : typing.Optional[str] + Native language (locale) of the personal voice (e.g. en-US, es-ES, etc.) + + avatar : typing.Optional[core.File] + See core.File for more documentation + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + CreatedVoice + A created voice + + Examples + -------- + import asyncio + + from speechify import AsyncSpeechify + + client = AsyncSpeechify( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.voices.create( + name="name", + gender="male", + consent="consent", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create( + name=name, + gender=gender, + sample=sample, + consent=consent, + locale=locale, + avatar=avatar, + request_options=request_options, + ) + return _response.data + + async def delete(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: + """ + Delete a personal (cloned) voice + + Parameters + ---------- + id : str + The ID of the voice to delete + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + import asyncio + + from speechify import AsyncSpeechify + + client = AsyncSpeechify( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.voices.delete( + id="id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete(id, request_options=request_options) + return _response.data + + async def download_sample( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> typing.AsyncIterator[bytes]: + """ + Download a personal (cloned) voice sample + + Parameters + ---------- + id : str + The ID of the voice to download sample for + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. You can pass in configuration such as `chunk_size`, and more to customize the request and response. + + Returns + ------- + typing.AsyncIterator[bytes] + Voice sample audio file + + Examples + -------- + import asyncio + + from speechify import AsyncSpeechify + + client = AsyncSpeechify( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.voices.download_sample( + id="id", + ) + + + asyncio.run(main()) + """ + async with self._raw_client.download_sample(id, request_options=request_options) as r: + async for _chunk in r.data: + yield _chunk diff --git a/src/speechify/voices/raw_client.py b/src/speechify/voices/raw_client.py new file mode 100644 index 0000000..84ae632 --- /dev/null +++ b/src/speechify/voices/raw_client.py @@ -0,0 +1,1093 @@ +# This file was auto-generated by Fern from our API Definition. + +import contextlib +import typing +from json.decoder import JSONDecodeError + +from .. import core +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import encode_path_param +from ..core.parse_error import ParsingError +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from ..errors.bad_gateway_error import BadGatewayError +from ..errors.bad_request_error import BadRequestError +from ..errors.forbidden_error import ForbiddenError +from ..errors.internal_server_error import InternalServerError +from ..errors.not_found_error import NotFoundError +from ..errors.payment_required_error import PaymentRequiredError +from ..errors.service_unavailable_error import ServiceUnavailableError +from ..errors.too_many_requests_error import TooManyRequestsError +from ..errors.unauthorized_error import UnauthorizedError +from ..errors.unprocessable_entity_error import UnprocessableEntityError +from ..types.created_voice import CreatedVoice +from ..types.error import Error +from ..types.get_voice import GetVoice +from .types.create_voices_request_gender import CreateVoicesRequestGender +from pydantic import ValidationError + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawVoicesClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> HttpResponse[typing.List[GetVoice]]: + """ + Gets the list of voices available for the user + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[typing.List[GetVoice]] + A list of voices + """ + _response = self._client_wrapper.httpx_client.request( + "v1/voices", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + typing.List[GetVoice], + parse_obj_as( + type_=typing.List[GetVoice], # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def create( + self, + *, + name: str, + gender: CreateVoicesRequestGender, + sample: core.File, + consent: str, + locale: typing.Optional[str] = OMIT, + avatar: typing.Optional[core.File] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[CreatedVoice]: + """ + Create a personal (cloned) voice for the user + + Parameters + ---------- + name : str + Name of the personal voice + + gender : CreateVoicesRequestGender + Gender marker for the personal voice + male GenderMale + female GenderFemale + notSpecified GenderNotSpecified + + sample : core.File + See core.File for more documentation + + consent : str + A **string** representing the user consent information in JSON format + This should include the fullName and email of the consenting individual. + For example, `{"fullName": "John Doe", "email": "john@example.com"}` + + locale : typing.Optional[str] + Native language (locale) of the personal voice (e.g. en-US, es-ES, etc.) + + avatar : typing.Optional[core.File] + See core.File for more documentation + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[CreatedVoice] + A created voice + """ + _response = self._client_wrapper.httpx_client.request( + "v1/voices", + method="POST", + data={ + "name": name, + "locale": locale, + "gender": gender, + "consent": consent, + }, + files={ + "sample": sample, + **({"avatar": avatar} if avatar is not None else {}), + }, + request_options=request_options, + omit=OMIT, + force_multipart=True, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + CreatedVoice, + parse_obj_as( + type_=CreatedVoice, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 502: + raise BadGatewayError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 503: + raise ServiceUnavailableError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def delete(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> HttpResponse[None]: + """ + Delete a personal (cloned) voice + + Parameters + ---------- + id : str + The ID of the voice to delete + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[None] + """ + _response = self._client_wrapper.httpx_client.request( + f"v1/voices/{encode_path_param(id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return HttpResponse(response=_response, data=None) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 502: + raise BadGatewayError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 503: + raise ServiceUnavailableError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + @contextlib.contextmanager + def download_sample( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> typing.Iterator[HttpResponse[typing.Iterator[bytes]]]: + """ + Download a personal (cloned) voice sample + + Parameters + ---------- + id : str + The ID of the voice to download sample for + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. You can pass in configuration such as `chunk_size`, and more to customize the request and response. + + Returns + ------- + typing.Iterator[HttpResponse[typing.Iterator[bytes]]] + Voice sample audio file + """ + with self._client_wrapper.httpx_client.stream( + f"v1/voices/{encode_path_param(id)}/sample", + method="GET", + request_options=request_options, + ) as _response: + + def _stream() -> HttpResponse[typing.Iterator[bytes]]: + try: + if 200 <= _response.status_code < 300: + _chunk_size = request_options.get("chunk_size", None) if request_options is not None else None + return HttpResponse( + response=_response, data=(_chunk for _chunk in _response.iter_bytes(chunk_size=_chunk_size)) + ) + _response.read() + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 502: + raise BadGatewayError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 503: + raise ServiceUnavailableError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.text + ) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.json(), + cause=e, + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + yield _stream() + + +class AsyncRawVoicesClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[typing.List[GetVoice]]: + """ + Gets the list of voices available for the user + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[typing.List[GetVoice]] + A list of voices + """ + _response = await self._client_wrapper.httpx_client.request( + "v1/voices", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + typing.List[GetVoice], + parse_obj_as( + type_=typing.List[GetVoice], # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def create( + self, + *, + name: str, + gender: CreateVoicesRequestGender, + sample: core.File, + consent: str, + locale: typing.Optional[str] = OMIT, + avatar: typing.Optional[core.File] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[CreatedVoice]: + """ + Create a personal (cloned) voice for the user + + Parameters + ---------- + name : str + Name of the personal voice + + gender : CreateVoicesRequestGender + Gender marker for the personal voice + male GenderMale + female GenderFemale + notSpecified GenderNotSpecified + + sample : core.File + See core.File for more documentation + + consent : str + A **string** representing the user consent information in JSON format + This should include the fullName and email of the consenting individual. + For example, `{"fullName": "John Doe", "email": "john@example.com"}` + + locale : typing.Optional[str] + Native language (locale) of the personal voice (e.g. en-US, es-ES, etc.) + + avatar : typing.Optional[core.File] + See core.File for more documentation + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[CreatedVoice] + A created voice + """ + _response = await self._client_wrapper.httpx_client.request( + "v1/voices", + method="POST", + data={ + "name": name, + "locale": locale, + "gender": gender, + "consent": consent, + }, + files={ + "sample": sample, + **({"avatar": avatar} if avatar is not None else {}), + }, + request_options=request_options, + omit=OMIT, + force_multipart=True, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + CreatedVoice, + parse_obj_as( + type_=CreatedVoice, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 502: + raise BadGatewayError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 503: + raise ServiceUnavailableError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def delete( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[None]: + """ + Delete a personal (cloned) voice + + Parameters + ---------- + id : str + The ID of the voice to delete + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[None] + """ + _response = await self._client_wrapper.httpx_client.request( + f"v1/voices/{encode_path_param(id)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return AsyncHttpResponse(response=_response, data=None) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 502: + raise BadGatewayError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 503: + raise ServiceUnavailableError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + @contextlib.asynccontextmanager + async def download_sample( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> typing.AsyncIterator[AsyncHttpResponse[typing.AsyncIterator[bytes]]]: + """ + Download a personal (cloned) voice sample + + Parameters + ---------- + id : str + The ID of the voice to download sample for + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. You can pass in configuration such as `chunk_size`, and more to customize the request and response. + + Returns + ------- + typing.AsyncIterator[AsyncHttpResponse[typing.AsyncIterator[bytes]]] + Voice sample audio file + """ + async with self._client_wrapper.httpx_client.stream( + f"v1/voices/{encode_path_param(id)}/sample", + method="GET", + request_options=request_options, + ) as _response: + + async def _stream() -> AsyncHttpResponse[typing.AsyncIterator[bytes]]: + try: + if 200 <= _response.status_code < 300: + _chunk_size = request_options.get("chunk_size", None) if request_options is not None else None + return AsyncHttpResponse( + response=_response, + data=(_chunk async for _chunk in _response.aiter_bytes(chunk_size=_chunk_size)), + ) + await _response.aread() + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 502: + raise BadGatewayError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 503: + raise ServiceUnavailableError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.text + ) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.json(), + cause=e, + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + yield await _stream() diff --git a/src/speechify/voices/types/__init__.py b/src/speechify/voices/types/__init__.py new file mode 100644 index 0000000..caa91ac --- /dev/null +++ b/src/speechify/voices/types/__init__.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .create_voices_request_gender import CreateVoicesRequestGender +_dynamic_imports: typing.Dict[str, str] = {"CreateVoicesRequestGender": ".create_voices_request_gender"} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["CreateVoicesRequestGender"] diff --git a/src/speechify/tts/voices/types/create_voices_request_gender.py b/src/speechify/voices/types/create_voices_request_gender.py similarity index 100% rename from src/speechify/tts/voices/types/create_voices_request_gender.py rename to src/speechify/voices/types/create_voices_request_gender.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..25710db --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,21 @@ +import pytest + + +def _has_httpx_aiohttp() -> bool: + """Check if httpx_aiohttp is importable.""" + try: + import httpx_aiohttp # type: ignore[import-not-found] # noqa: F401 + + return True + except ImportError: + return False + + +def pytest_collection_modifyitems(config: pytest.Config, items: list) -> None: + """Auto-skip @pytest.mark.aiohttp tests when httpx_aiohttp is not installed.""" + if _has_httpx_aiohttp(): + return + skip_aiohttp = pytest.mark.skip(reason="httpx_aiohttp not installed") + for item in items: + if "aiohttp" in item.keywords: + item.add_marker(skip_aiohttp) diff --git a/tests/custom/test_client.py b/tests/custom/test_client.py index 73f811f..ab04ce6 100644 --- a/tests/custom/test_client.py +++ b/tests/custom/test_client.py @@ -4,4 +4,4 @@ # Get started with writing tests with pytest at https://docs.pytest.org @pytest.mark.skip(reason="Unimplemented") def test_client() -> None: - assert True == True + assert True diff --git a/tests/test_aiohttp_autodetect.py b/tests/test_aiohttp_autodetect.py new file mode 100644 index 0000000..228f3f2 --- /dev/null +++ b/tests/test_aiohttp_autodetect.py @@ -0,0 +1,116 @@ +import importlib +import sys +import unittest +from unittest import mock + +import httpx +import pytest + + +class TestMakeDefaultAsyncClientWithoutAiohttp(unittest.TestCase): + """Tests for _make_default_async_client when httpx_aiohttp is NOT installed.""" + + def test_returns_httpx_async_client(self) -> None: + """When httpx_aiohttp is not installed, returns plain httpx.AsyncClient.""" + with mock.patch.dict(sys.modules, {"httpx_aiohttp": None}): + from speechify.client import _make_default_async_client + + client = _make_default_async_client(timeout=60, follow_redirects=True) + self.assertIsInstance(client, httpx.AsyncClient) + self.assertEqual(client.timeout.read, 60) + self.assertTrue(client.follow_redirects) + + def test_follow_redirects_none(self) -> None: + """When follow_redirects is None, omits it from httpx.AsyncClient.""" + with mock.patch.dict(sys.modules, {"httpx_aiohttp": None}): + from speechify.client import _make_default_async_client + + client = _make_default_async_client(timeout=60, follow_redirects=None) + self.assertIsInstance(client, httpx.AsyncClient) + self.assertFalse(client.follow_redirects) + + def test_explicit_httpx_client_bypasses_autodetect(self) -> None: + """When user passes httpx_client explicitly, _make_default_async_client is not called.""" + + explicit_client = httpx.AsyncClient(timeout=120) + with mock.patch("speechify.client._make_default_async_client") as mock_make: + # Replicate the generated conditional: httpx_client if httpx_client is not None else _make_default_async_client(...) + result = explicit_client if explicit_client is not None else mock_make(timeout=60, follow_redirects=True) + mock_make.assert_not_called() + self.assertIs(result, explicit_client) + + +@pytest.mark.aiohttp +class TestMakeDefaultAsyncClientWithAiohttp(unittest.TestCase): + """Tests for _make_default_async_client when httpx_aiohttp IS installed.""" + + def test_returns_aiohttp_client(self) -> None: + """When httpx_aiohttp is installed, returns HttpxAiohttpClient.""" + import httpx_aiohttp # type: ignore[import-not-found] + + from speechify.client import _make_default_async_client + + client = _make_default_async_client(timeout=60, follow_redirects=True) + self.assertIsInstance(client, httpx_aiohttp.HttpxAiohttpClient) + self.assertEqual(client.timeout.read, 60) + self.assertTrue(client.follow_redirects) + + def test_follow_redirects_none(self) -> None: + """When httpx_aiohttp is installed and follow_redirects is None, omits it.""" + import httpx_aiohttp # type: ignore[import-not-found] + + from speechify.client import _make_default_async_client + + client = _make_default_async_client(timeout=60, follow_redirects=None) + self.assertIsInstance(client, httpx_aiohttp.HttpxAiohttpClient) + self.assertFalse(client.follow_redirects) + + +class TestDefaultClientsWithoutAiohttp(unittest.TestCase): + """Tests for _default_clients.py convenience classes (no aiohttp).""" + + def test_default_async_httpx_client_defaults(self) -> None: + """DefaultAsyncHttpxClient applies SDK defaults.""" + from speechify._default_clients import SDK_DEFAULT_TIMEOUT, DefaultAsyncHttpxClient + + client = DefaultAsyncHttpxClient() + self.assertIsInstance(client, httpx.AsyncClient) + self.assertEqual(client.timeout.read, SDK_DEFAULT_TIMEOUT) + self.assertTrue(client.follow_redirects) + + def test_default_async_httpx_client_overrides(self) -> None: + """DefaultAsyncHttpxClient allows overriding defaults.""" + from speechify._default_clients import DefaultAsyncHttpxClient + + client = DefaultAsyncHttpxClient(timeout=30, follow_redirects=False) + self.assertEqual(client.timeout.read, 30) + self.assertFalse(client.follow_redirects) + + def test_default_aiohttp_client_raises_without_package(self) -> None: + """DefaultAioHttpClient raises RuntimeError when httpx_aiohttp not installed.""" + import speechify._default_clients + + with mock.patch.dict(sys.modules, {"httpx_aiohttp": None}): + importlib.reload(speechify._default_clients) + + with self.assertRaises(RuntimeError) as ctx: + speechify._default_clients.DefaultAioHttpClient() + self.assertIn("pip install speechify-api[aiohttp]", str(ctx.exception)) + + importlib.reload(speechify._default_clients) + + +@pytest.mark.aiohttp +class TestDefaultClientsWithAiohttp(unittest.TestCase): + """Tests for _default_clients.py when httpx_aiohttp IS installed.""" + + def test_default_aiohttp_client_defaults(self) -> None: + """DefaultAioHttpClient works when httpx_aiohttp is installed.""" + import httpx_aiohttp # type: ignore[import-not-found] + + from speechify._default_clients import SDK_DEFAULT_TIMEOUT, DefaultAioHttpClient + + client = DefaultAioHttpClient() + self.assertIsInstance(client, httpx_aiohttp.HttpxAiohttpClient) + self.assertEqual(client.timeout.read, SDK_DEFAULT_TIMEOUT) + self.assertTrue(client.follow_redirects) diff --git a/tests/utils/assets/models/__init__.py b/tests/utils/assets/models/__init__.py index 3a1c852..2cf0126 100644 --- a/tests/utils/assets/models/__init__.py +++ b/tests/utils/assets/models/__init__.py @@ -5,7 +5,7 @@ from .circle import CircleParams from .object_with_defaults import ObjectWithDefaultsParams from .object_with_optional_field import ObjectWithOptionalFieldParams -from .shape import ShapeParams, Shape_CircleParams, Shape_SquareParams +from .shape import Shape_CircleParams, Shape_SquareParams, ShapeParams from .square import SquareParams from .undiscriminated_shape import UndiscriminatedShapeParams diff --git a/tests/utils/assets/models/circle.py b/tests/utils/assets/models/circle.py index 897a897..d889893 100644 --- a/tests/utils/assets/models/circle.py +++ b/tests/utils/assets/models/circle.py @@ -3,7 +3,7 @@ # This file was auto-generated by Fern from our API Definition. import typing_extensions -import typing_extensions + from speechify.core.serialization import FieldMetadata diff --git a/tests/utils/assets/models/object_with_defaults.py b/tests/utils/assets/models/object_with_defaults.py index ef14f7b..a977b1d 100644 --- a/tests/utils/assets/models/object_with_defaults.py +++ b/tests/utils/assets/models/object_with_defaults.py @@ -3,7 +3,6 @@ # This file was auto-generated by Fern from our API Definition. import typing_extensions -import typing_extensions class ObjectWithDefaultsParams(typing_extensions.TypedDict): diff --git a/tests/utils/assets/models/object_with_optional_field.py b/tests/utils/assets/models/object_with_optional_field.py index 392fa17..2a3b1cb 100644 --- a/tests/utils/assets/models/object_with_optional_field.py +++ b/tests/utils/assets/models/object_with_optional_field.py @@ -2,16 +2,17 @@ # This file was auto-generated by Fern from our API Definition. -import typing_extensions -import typing -import typing_extensions -from speechify.core.serialization import FieldMetadata import datetime as dt +import typing import uuid + +import typing_extensions from .color import Color from .shape import ShapeParams from .undiscriminated_shape import UndiscriminatedShapeParams +from speechify.core.serialization import FieldMetadata + class ObjectWithOptionalFieldParams(typing_extensions.TypedDict): literal: typing.Literal["lit_one"] diff --git a/tests/utils/assets/models/shape.py b/tests/utils/assets/models/shape.py index 297ae67..c5f3a7b 100644 --- a/tests/utils/assets/models/shape.py +++ b/tests/utils/assets/models/shape.py @@ -3,9 +3,11 @@ # This file was auto-generated by Fern from our API Definition. from __future__ import annotations -import typing_extensions -import typing_extensions + import typing + +import typing_extensions + from speechify.core.serialization import FieldMetadata diff --git a/tests/utils/assets/models/square.py b/tests/utils/assets/models/square.py index 603b279..1909705 100644 --- a/tests/utils/assets/models/square.py +++ b/tests/utils/assets/models/square.py @@ -3,7 +3,7 @@ # This file was auto-generated by Fern from our API Definition. import typing_extensions -import typing_extensions + from speechify.core.serialization import FieldMetadata diff --git a/tests/utils/assets/models/undiscriminated_shape.py b/tests/utils/assets/models/undiscriminated_shape.py index 68876a2..99f12b3 100644 --- a/tests/utils/assets/models/undiscriminated_shape.py +++ b/tests/utils/assets/models/undiscriminated_shape.py @@ -3,6 +3,7 @@ # This file was auto-generated by Fern from our API Definition. import typing + from .circle import CircleParams from .square import SquareParams diff --git a/tests/utils/test_http_client.py b/tests/utils/test_http_client.py index 9de0cd6..763e08e 100644 --- a/tests/utils/test_http_client.py +++ b/tests/utils/test_http_client.py @@ -1,13 +1,60 @@ # This file was auto-generated by Fern from our API Definition. -from speechify.core.http_client import get_request_body +from typing import Any, Dict +from unittest.mock import AsyncMock, MagicMock, patch + +import httpx +import pytest + +from speechify.core.http_client import ( + AsyncHttpClient, + HttpClient, + _build_url, + _should_retry, + get_request_body, + remove_none_from_dict, +) from speechify.core.request_options import RequestOptions +# Stub clients for testing HttpClient and AsyncHttpClient +class _DummySyncClient: + """A minimal stub for httpx.Client that records request arguments.""" + + def __init__(self) -> None: + self.last_request_kwargs: Dict[str, Any] = {} + + def request(self, **kwargs: Any) -> "_DummyResponse": + self.last_request_kwargs = kwargs + return _DummyResponse() + + +class _DummyAsyncClient: + """A minimal stub for httpx.AsyncClient that records request arguments.""" + + def __init__(self) -> None: + self.last_request_kwargs: Dict[str, Any] = {} + + async def request(self, **kwargs: Any) -> "_DummyResponse": + self.last_request_kwargs = kwargs + return _DummyResponse() + + +class _DummyResponse: + """A minimal stub for httpx.Response.""" + + status_code = 200 + headers: Dict[str, str] = {} + + def get_request_options() -> RequestOptions: return {"additional_body_parameters": {"see you": "later"}} +def get_request_options_with_none() -> RequestOptions: + return {"additional_body_parameters": {"see you": "later", "optional": None}} + + def test_get_json_request_body() -> None: json_body, data_body = get_request_body(json={"hello": "world"}, data=None, request_options=None, omit=None) assert json_body == {"hello": "world"} @@ -48,14 +95,600 @@ def test_get_none_request_body() -> None: def test_get_empty_json_request_body() -> None: + """Test that implicit empty bodies (json=None) are collapsed to None.""" unrelated_request_options: RequestOptions = {"max_retries": 3} json_body, data_body = get_request_body(json=None, data=None, request_options=unrelated_request_options, omit=None) assert json_body is None assert data_body is None - json_body_extras, data_body_extras = get_request_body( - json={}, data=None, request_options=unrelated_request_options, omit=None + +def test_explicit_empty_json_body_is_preserved() -> None: + """Test that explicit empty bodies (json={}) are preserved and sent as {}. + + This is important for endpoints where the request body is required but all + fields are optional. The server expects valid JSON ({}) not an empty body. + """ + unrelated_request_options: RequestOptions = {"max_retries": 3} + + # Explicit json={} should be preserved + json_body, data_body = get_request_body(json={}, data=None, request_options=unrelated_request_options, omit=None) + assert json_body == {} + assert data_body is None + + # Explicit data={} should also be preserved + json_body2, data_body2 = get_request_body(json=None, data={}, request_options=unrelated_request_options, omit=None) + assert json_body2 is None + assert data_body2 == {} + + +def test_json_body_preserves_none_values() -> None: + """Test that JSON bodies preserve None values (they become JSON null).""" + json_body, data_body = get_request_body( + json={"hello": "world", "optional": None}, data=None, request_options=None, omit=None ) + # JSON bodies should preserve None values + assert json_body == {"hello": "world", "optional": None} + assert data_body is None - assert json_body_extras is None - assert data_body_extras is None + +def test_data_body_preserves_none_values_without_multipart() -> None: + """Test that data bodies preserve None values when not using multipart. + + The filtering of None values happens in HttpClient.request/stream methods, + not in get_request_body. This test verifies get_request_body doesn't filter None. + """ + json_body, data_body = get_request_body( + json=None, data={"hello": "world", "optional": None}, request_options=None, omit=None + ) + # get_request_body should preserve None values in data body + # The filtering happens later in HttpClient.request when multipart is detected + assert data_body == {"hello": "world", "optional": None} + assert json_body is None + + +def test_remove_none_from_dict_filters_none_values() -> None: + """Test that remove_none_from_dict correctly filters out None values.""" + original = {"hello": "world", "optional": None, "another": "value", "also_none": None} + filtered = remove_none_from_dict(original) + assert filtered == {"hello": "world", "another": "value"} + # Original should not be modified + assert original == {"hello": "world", "optional": None, "another": "value", "also_none": None} + + +def test_remove_none_from_dict_empty_dict() -> None: + """Test that remove_none_from_dict handles empty dict.""" + assert remove_none_from_dict({}) == {} + + +def test_remove_none_from_dict_all_none() -> None: + """Test that remove_none_from_dict handles dict with all None values.""" + assert remove_none_from_dict({"a": None, "b": None}) == {} + + +def test_http_client_does_not_pass_empty_params_list() -> None: + """Test that HttpClient passes params=None when params are empty. + + This prevents httpx from stripping existing query parameters from the URL, + which happens when params=[] or params={} is passed. + """ + dummy_client = _DummySyncClient() + http_client = HttpClient( + httpx_client=dummy_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + ) + + # Use a path with query params (e.g., pagination cursor URL) + http_client.request( + path="resource?after=123", + method="GET", + params=None, + request_options=None, + ) + + # We care that httpx receives params=None, not [] or {} + assert "params" in dummy_client.last_request_kwargs + assert dummy_client.last_request_kwargs["params"] is None + + # Verify the query string in the URL is preserved + url = str(dummy_client.last_request_kwargs["url"]) + assert "after=123" in url, f"Expected query param 'after=123' in URL, got: {url}" + + +def test_http_client_passes_encoded_params_when_present() -> None: + """Test that HttpClient passes encoded params when params are provided.""" + dummy_client = _DummySyncClient() + http_client = HttpClient( + httpx_client=dummy_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com/resource", + ) + + http_client.request( + path="", + method="GET", + params={"after": "456"}, + request_options=None, + ) + + params = dummy_client.last_request_kwargs["params"] + # For a simple dict, encode_query should give a single (key, value) tuple + assert params == [("after", "456")] + + +@pytest.mark.asyncio +async def test_async_http_client_does_not_pass_empty_params_list() -> None: + """Test that AsyncHttpClient passes params=None when params are empty. + + This prevents httpx from stripping existing query parameters from the URL, + which happens when params=[] or params={} is passed. + """ + dummy_client = _DummyAsyncClient() + http_client = AsyncHttpClient( + httpx_client=dummy_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + async_base_headers=None, + ) + + # Use a path with query params (e.g., pagination cursor URL) + await http_client.request( + path="resource?after=123", + method="GET", + params=None, + request_options=None, + ) + + # We care that httpx receives params=None, not [] or {} + assert "params" in dummy_client.last_request_kwargs + assert dummy_client.last_request_kwargs["params"] is None + + # Verify the query string in the URL is preserved + url = str(dummy_client.last_request_kwargs["url"]) + assert "after=123" in url, f"Expected query param 'after=123' in URL, got: {url}" + + +@pytest.mark.asyncio +async def test_async_http_client_passes_encoded_params_when_present() -> None: + """Test that AsyncHttpClient passes encoded params when params are provided.""" + dummy_client = _DummyAsyncClient() + http_client = AsyncHttpClient( + httpx_client=dummy_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com/resource", + async_base_headers=None, + ) + + await http_client.request( + path="", + method="GET", + params={"after": "456"}, + request_options=None, + ) + + params = dummy_client.last_request_kwargs["params"] + # For a simple dict, encode_query should give a single (key, value) tuple + assert params == [("after", "456")] + + +def test_basic_url_joining() -> None: + """Test basic URL joining with a simple base URL and path.""" + result = _build_url("https://api.example.com", "/users") + assert result == "https://api.example.com/users" + + +def test_basic_url_joining_trailing_slash() -> None: + """Test basic URL joining with a simple base URL and path.""" + result = _build_url("https://api.example.com/", "/users") + assert result == "https://api.example.com/users" + + +def test_preserves_base_url_path_prefix() -> None: + """Test that path prefixes in base URL are preserved. + + This is the critical bug fix - urllib.parse.urljoin() would strip + the path prefix when the path starts with '/'. + """ + result = _build_url("https://cloud.example.com/org/tenant/api", "/users") + assert result == "https://cloud.example.com/org/tenant/api/users" + + +def test_preserves_base_url_path_prefix_trailing_slash() -> None: + """Test that path prefixes in base URL are preserved.""" + result = _build_url("https://cloud.example.com/org/tenant/api/", "/users") + assert result == "https://cloud.example.com/org/tenant/api/users" + + +# --------------------------------------------------------------------------- +# Connection error retry tests +# --------------------------------------------------------------------------- + + +def _make_sync_http_client(mock_client: Any) -> HttpClient: + return HttpClient( + httpx_client=mock_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + ) + + +def _make_async_http_client(mock_client: Any) -> AsyncHttpClient: + return AsyncHttpClient( + httpx_client=mock_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + async_base_headers=None, + ) + + +@patch("speechify.core.http_client.time.sleep", return_value=None) +def test_sync_retries_on_connect_error(mock_sleep: MagicMock) -> None: + """Sync: connection error retries on httpx.ConnectError.""" + mock_client = MagicMock() + mock_client.request.side_effect = [ + httpx.ConnectError("connection failed"), + _DummyResponse(), + ] + http_client = _make_sync_http_client(mock_client) + + response = http_client.request(path="/test", method="GET") + + assert response.status_code == 200 + assert mock_client.request.call_count == 2 + mock_sleep.assert_called_once() + + +@patch("speechify.core.http_client.time.sleep", return_value=None) +def test_sync_retries_on_remote_protocol_error(mock_sleep: MagicMock) -> None: + """Sync: connection error retries on httpx.RemoteProtocolError.""" + mock_client = MagicMock() + mock_client.request.side_effect = [ + httpx.RemoteProtocolError("Remote end closed connection without response"), + _DummyResponse(), + ] + http_client = _make_sync_http_client(mock_client) + + response = http_client.request(path="/test", method="GET") + + assert response.status_code == 200 + assert mock_client.request.call_count == 2 + mock_sleep.assert_called_once() + + +@patch("speechify.core.http_client.time.sleep", return_value=None) +def test_sync_connection_error_exhausts_retries(mock_sleep: MagicMock) -> None: + """Sync: connection error exhausts retries then raises.""" + mock_client = MagicMock() + mock_client.request.side_effect = httpx.ConnectError("connection failed") + http_client = _make_sync_http_client(mock_client) + + with pytest.raises(httpx.ConnectError): + http_client.request( + path="/test", + method="GET", + request_options={"max_retries": 2}, + ) + + # 1 initial + 2 retries = 3 total attempts + assert mock_client.request.call_count == 3 + assert mock_sleep.call_count == 2 + + +@patch("speechify.core.http_client.time.sleep", return_value=None) +def test_sync_connection_error_respects_max_retries_zero(mock_sleep: MagicMock) -> None: + """Sync: connection error respects max_retries=0.""" + mock_client = MagicMock() + mock_client.request.side_effect = httpx.ConnectError("connection failed") + http_client = _make_sync_http_client(mock_client) + + with pytest.raises(httpx.ConnectError): + http_client.request( + path="/test", + method="GET", + request_options={"max_retries": 0}, + ) + + # No retries, just the initial attempt + assert mock_client.request.call_count == 1 + mock_sleep.assert_not_called() + + +@pytest.mark.asyncio +@patch("speechify.core.http_client.asyncio.sleep", new_callable=AsyncMock) +async def test_async_retries_on_connect_error(mock_sleep: AsyncMock) -> None: + """Async: connection error retries on httpx.ConnectError.""" + mock_client = MagicMock() + mock_client.request = AsyncMock( + side_effect=[ + httpx.ConnectError("connection failed"), + _DummyResponse(), + ] + ) + http_client = _make_async_http_client(mock_client) + + response = await http_client.request(path="/test", method="GET") + + assert response.status_code == 200 + assert mock_client.request.call_count == 2 + mock_sleep.assert_called_once() + + +@pytest.mark.asyncio +@patch("speechify.core.http_client.asyncio.sleep", new_callable=AsyncMock) +async def test_async_retries_on_remote_protocol_error(mock_sleep: AsyncMock) -> None: + """Async: connection error retries on httpx.RemoteProtocolError.""" + mock_client = MagicMock() + mock_client.request = AsyncMock( + side_effect=[ + httpx.RemoteProtocolError("Remote end closed connection without response"), + _DummyResponse(), + ] + ) + http_client = _make_async_http_client(mock_client) + + response = await http_client.request(path="/test", method="GET") + + assert response.status_code == 200 + assert mock_client.request.call_count == 2 + mock_sleep.assert_called_once() + + +@pytest.mark.asyncio +@patch("speechify.core.http_client.asyncio.sleep", new_callable=AsyncMock) +async def test_async_connection_error_exhausts_retries(mock_sleep: AsyncMock) -> None: + """Async: connection error exhausts retries then raises.""" + mock_client = MagicMock() + mock_client.request = AsyncMock(side_effect=httpx.ConnectError("connection failed")) + http_client = _make_async_http_client(mock_client) + + with pytest.raises(httpx.ConnectError): + await http_client.request( + path="/test", + method="GET", + request_options={"max_retries": 2}, + ) + + # 1 initial + 2 retries = 3 total attempts + assert mock_client.request.call_count == 3 + assert mock_sleep.call_count == 2 + + +# --------------------------------------------------------------------------- +# base_max_retries constructor parameter tests +# --------------------------------------------------------------------------- + + +def test_sync_http_client_default_base_max_retries() -> None: + """HttpClient defaults to base_max_retries=2.""" + http_client = HttpClient( + httpx_client=MagicMock(), # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + ) + assert http_client.base_max_retries == 2 + + +def test_async_http_client_default_base_max_retries() -> None: + """AsyncHttpClient defaults to base_max_retries=2.""" + http_client = AsyncHttpClient( + httpx_client=MagicMock(), # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + ) + assert http_client.base_max_retries == 2 + + +def test_sync_http_client_custom_base_max_retries() -> None: + """HttpClient accepts a custom base_max_retries value.""" + http_client = HttpClient( + httpx_client=MagicMock(), # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_max_retries=5, + ) + assert http_client.base_max_retries == 5 + + +def test_async_http_client_custom_base_max_retries() -> None: + """AsyncHttpClient accepts a custom base_max_retries value.""" + http_client = AsyncHttpClient( + httpx_client=MagicMock(), # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_max_retries=5, + ) + assert http_client.base_max_retries == 5 + + +@patch("speechify.core.http_client.time.sleep", return_value=None) +def test_sync_base_max_retries_zero_disables_retries(mock_sleep: MagicMock) -> None: + """Sync: base_max_retries=0 disables retries when no request_options override.""" + mock_client = MagicMock() + mock_client.request.side_effect = httpx.ConnectError("connection failed") + http_client = HttpClient( + httpx_client=mock_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + base_max_retries=0, + ) + + with pytest.raises(httpx.ConnectError): + http_client.request(path="/test", method="GET") + + # No retries, just the initial attempt + assert mock_client.request.call_count == 1 + mock_sleep.assert_not_called() + + +@pytest.mark.asyncio +@patch("speechify.core.http_client.asyncio.sleep", new_callable=AsyncMock) +async def test_async_base_max_retries_zero_disables_retries(mock_sleep: AsyncMock) -> None: + """Async: base_max_retries=0 disables retries when no request_options override.""" + mock_client = MagicMock() + mock_client.request = AsyncMock(side_effect=httpx.ConnectError("connection failed")) + http_client = AsyncHttpClient( + httpx_client=mock_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + base_max_retries=0, + ) + + with pytest.raises(httpx.ConnectError): + await http_client.request(path="/test", method="GET") + + # No retries, just the initial attempt + assert mock_client.request.call_count == 1 + mock_sleep.assert_not_called() + + +@patch("speechify.core.http_client.time.sleep", return_value=None) +def test_sync_request_options_override_base_max_retries(mock_sleep: MagicMock) -> None: + """Sync: request_options max_retries overrides base_max_retries.""" + mock_client = MagicMock() + mock_client.request.side_effect = [ + httpx.ConnectError("connection failed"), + httpx.ConnectError("connection failed"), + _DummyResponse(), + ] + http_client = HttpClient( + httpx_client=mock_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + base_max_retries=0, # base says no retries + ) + + # But request_options overrides to allow 2 retries + response = http_client.request( + path="/test", + method="GET", + request_options={"max_retries": 2}, + ) + + assert response.status_code == 200 + # 1 initial + 2 retries = 3 total attempts + assert mock_client.request.call_count == 3 + + +@pytest.mark.asyncio +@patch("speechify.core.http_client.asyncio.sleep", new_callable=AsyncMock) +async def test_async_request_options_override_base_max_retries(mock_sleep: AsyncMock) -> None: + """Async: request_options max_retries overrides base_max_retries.""" + mock_client = MagicMock() + mock_client.request = AsyncMock( + side_effect=[ + httpx.ConnectError("connection failed"), + httpx.ConnectError("connection failed"), + _DummyResponse(), + ] + ) + http_client = AsyncHttpClient( + httpx_client=mock_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + base_max_retries=0, # base says no retries + ) + + # But request_options overrides to allow 2 retries + response = await http_client.request( + path="/test", + method="GET", + request_options={"max_retries": 2}, + ) + + assert response.status_code == 200 + # 1 initial + 2 retries = 3 total attempts + assert mock_client.request.call_count == 3 + + +@patch("speechify.core.http_client.time.sleep", return_value=None) +def test_sync_base_max_retries_used_as_default(mock_sleep: MagicMock) -> None: + """Sync: base_max_retries is used when request_options has no max_retries.""" + mock_client = MagicMock() + mock_client.request.side_effect = [ + httpx.ConnectError("fail"), + httpx.ConnectError("fail"), + httpx.ConnectError("fail"), + _DummyResponse(), + ] + http_client = HttpClient( + httpx_client=mock_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + base_max_retries=3, + ) + + response = http_client.request(path="/test", method="GET") + + assert response.status_code == 200 + # 1 initial + 3 retries = 4 total attempts + assert mock_client.request.call_count == 4 + + +@pytest.mark.asyncio +@patch("speechify.core.http_client.asyncio.sleep", new_callable=AsyncMock) +async def test_async_base_max_retries_used_as_default(mock_sleep: AsyncMock) -> None: + """Async: base_max_retries is used when request_options has no max_retries.""" + mock_client = MagicMock() + mock_client.request = AsyncMock( + side_effect=[ + httpx.ConnectError("fail"), + httpx.ConnectError("fail"), + httpx.ConnectError("fail"), + _DummyResponse(), + ] + ) + http_client = AsyncHttpClient( + httpx_client=mock_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + base_max_retries=3, + ) + + response = await http_client.request(path="/test", method="GET") + + assert response.status_code == 200 + # 1 initial + 3 retries = 4 total attempts + assert mock_client.request.call_count == 4 + + +# --------------------------------------------------------------------------- +# _should_retry unit tests +# --------------------------------------------------------------------------- + + +def _make_response(status_code: int) -> httpx.Response: + return httpx.Response(status_code=status_code, content=b"") + + +@pytest.mark.parametrize( + "status_code", + [408, 409, 429, 500, 501, 502, 503, 504, 599], +) +def test_should_retry_retryable_status_codes(status_code: int) -> None: + """Legacy mode: retries on 408, 409, 429, and all >= 500.""" + assert _should_retry(_make_response(status_code)) is True + + +@pytest.mark.parametrize( + "status_code", + [200, 201, 301, 400, 401, 403, 404], +) +def test_should_not_retry_non_retryable_status_codes(status_code: int) -> None: + assert _should_retry(_make_response(status_code)) is False + + +def test_should_retry_599_upper_boundary() -> None: + """Legacy mode retries on >= 500, which includes 599.""" + assert _should_retry(_make_response(599)) is True diff --git a/tests/utils/test_query_encoding.py b/tests/utils/test_query_encoding.py index 6442d36..8e9a65a 100644 --- a/tests/utils/test_query_encoding.py +++ b/tests/utils/test_query_encoding.py @@ -1,6 +1,5 @@ # This file was auto-generated by Fern from our API Definition. - from speechify.core.query_encoder import encode_query @@ -34,4 +33,4 @@ def test_query_encoding_deep_object_arrays() -> None: def test_encode_query_with_none() -> None: encoded = encode_query(None) - assert encoded == None + assert encoded is None diff --git a/tests/utils/test_serialization.py b/tests/utils/test_serialization.py index cdd3f22..f96038e 100644 --- a/tests/utils/test_serialization.py +++ b/tests/utils/test_serialization.py @@ -1,10 +1,10 @@ # This file was auto-generated by Fern from our API Definition. -from typing import List, Any +from typing import Any, List -from speechify.core.serialization import convert_and_respect_annotation_metadata -from .assets.models import ShapeParams, ObjectWithOptionalFieldParams +from .assets.models import ObjectWithOptionalFieldParams, ShapeParams +from speechify.core.serialization import convert_and_respect_annotation_metadata UNION_TEST: ShapeParams = {"radius_measurement": 1.0, "shape_type": "circle", "id": "1"} UNION_TEST_CONVERTED = {"shapeType": "circle", "radiusMeasurement": 1.0, "id": "1"}