From 2eaa7dbb883d8ae3ecf90da45dce26252a996f62 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 28 Nov 2025 03:22:24 +0000 Subject: [PATCH 01/17] fix: ensure streams are always closed --- src/spitch/_streaming.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/spitch/_streaming.py b/src/spitch/_streaming.py index feccb04..976c511 100644 --- a/src/spitch/_streaming.py +++ b/src/spitch/_streaming.py @@ -54,11 +54,12 @@ def __stream__(self) -> Iterator[_T]: process_data = self._client._process_response_data iterator = self._iter_events() - for sse in iterator: - yield process_data(data=sse.json(), cast_to=cast_to, response=response) - - # As we might not fully consume the response stream, we need to close it explicitly - response.close() + try: + for sse in iterator: + yield process_data(data=sse.json(), cast_to=cast_to, response=response) + finally: + # Ensure the response is closed even if the consumer doesn't read all data + response.close() def __enter__(self) -> Self: return self @@ -117,11 +118,12 @@ async def __stream__(self) -> AsyncIterator[_T]: process_data = self._client._process_response_data iterator = self._iter_events() - async for sse in iterator: - yield process_data(data=sse.json(), cast_to=cast_to, response=response) - - # As we might not fully consume the response stream, we need to close it explicitly - await response.aclose() + try: + async for sse in iterator: + yield process_data(data=sse.json(), cast_to=cast_to, response=response) + finally: + # Ensure the response is closed even if the consumer doesn't read all data + await response.aclose() async def __aenter__(self) -> Self: return self From ebbc73f41c762dc2adcbb3013ea695fdb3c50e2d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 28 Nov 2025 03:23:37 +0000 Subject: [PATCH 02/17] chore(deps): mypy 1.18.1 has a regression, pin to 1.17 --- pyproject.toml | 2 +- requirements-dev.lock | 4 +++- requirements.lock | 8 ++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index cb8bcd7..a986f76 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ managed = true # version pins are in requirements-dev.lock dev-dependencies = [ "pyright==1.1.399", - "mypy", + "mypy==1.17", "respx", "pytest", "pytest-asyncio", diff --git a/requirements-dev.lock b/requirements-dev.lock index fd8463b..00481a0 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -72,7 +72,7 @@ mdurl==0.1.2 multidict==6.5.0 # via aiohttp # via yarl -mypy==1.14.1 +mypy==1.17.0 mypy-extensions==1.0.0 # via mypy nodeenv==1.8.0 @@ -81,6 +81,8 @@ nox==2023.4.22 packaging==23.2 # via nox # via pytest +pathspec==0.12.1 + # via mypy platformdirs==3.11.0 # via virtualenv pluggy==1.5.0 diff --git a/requirements.lock b/requirements.lock index e137eed..5d9c3fc 100644 --- a/requirements.lock +++ b/requirements.lock @@ -55,22 +55,22 @@ multidict==6.5.0 propcache==0.3.2 # via aiohttp # via yarl -pydantic==2.11.9 +pydantic==2.12.5 # via spitch -pydantic-core==2.33.2 +pydantic-core==2.41.5 # via pydantic sniffio==1.3.0 # via anyio # via httpx # via spitch -typing-extensions==4.12.2 +typing-extensions==4.15.0 # via anyio # via multidict # via pydantic # via pydantic-core # via spitch # via typing-inspection -typing-inspection==0.4.1 +typing-inspection==0.4.2 # via pydantic yarl==1.20.0 # via aiohttp From 442785c97d9cb3ccee5403666a4cdbbf00338cea Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 05:11:42 +0000 Subject: [PATCH 03/17] chore: update lockfile --- pyproject.toml | 12 +++-- requirements-dev.lock | 109 +++++++++++++++++++++++------------------- requirements.lock | 32 ++++++------- 3 files changed, 82 insertions(+), 71 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a986f76..392d40e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,15 +7,17 @@ license = "Apache-2.0" authors = [ { name = "Spitch", email = "developer@spitch.app" }, ] + dependencies = [ - "httpx>=0.23.0, <1", - "pydantic>=1.9.0, <3", + "httpx>=0.23.0, <1", + "pydantic>=1.9.0, <3", "typing-extensions>=4.7, <5", - "anyio>=3.5.0, <5", - "distro>=1.7.0, <2", - "sniffio", + "anyio>=3.5.0, <5", + "distro>=1.7.0, <2", + "sniffio", "cached-property; python_version < '3.8'", ] + requires-python = ">= 3.9" classifiers = [ "Typing :: Typed", diff --git a/requirements-dev.lock b/requirements-dev.lock index 00481a0..9badb5b 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -12,40 +12,45 @@ -e file:. aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.12.13 +aiohttp==3.13.2 # via httpx-aiohttp # via spitch -aiosignal==1.3.2 +aiosignal==1.4.0 # via aiohttp -annotated-types==0.6.0 +annotated-types==0.7.0 # via pydantic -anyio==4.4.0 +anyio==4.12.0 # via httpx # via spitch -argcomplete==3.1.2 +argcomplete==3.6.3 # via nox async-timeout==5.0.1 # via aiohttp -attrs==25.3.0 +attrs==25.4.0 # via aiohttp -certifi==2023.7.22 + # via nox +backports-asyncio-runner==1.2.0 + # via pytest-asyncio +certifi==2025.11.12 # via httpcore # via httpx -colorlog==6.7.0 +colorlog==6.10.1 + # via nox +dependency-groups==1.3.1 # via nox -dirty-equals==0.6.0 -distlib==0.3.7 +dirty-equals==0.11 +distlib==0.4.0 # via virtualenv -distro==1.8.0 +distro==1.9.0 # via spitch -exceptiongroup==1.2.2 +exceptiongroup==1.3.1 # via anyio # via pytest -execnet==2.1.1 +execnet==2.1.2 # via pytest-xdist -filelock==3.12.4 +filelock==3.19.1 # via virtualenv -frozenlist==1.7.0 +frozenlist==1.8.0 # via aiohttp # via aiosignal h11==0.16.0 @@ -58,83 +63,87 @@ httpx==0.27.2 # via spitch httpx-aiohttp==0.1.9 # via spitch -idna==3.4 +humanize==4.13.0 + # via nox +idna==3.11 # via anyio # via httpx # via yarl -importlib-metadata==7.0.0 -iniconfig==2.0.0 +importlib-metadata==8.7.0 +iniconfig==2.1.0 # via pytest markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -multidict==6.5.0 +multidict==6.7.0 # via aiohttp # via yarl mypy==1.17.0 -mypy-extensions==1.0.0 +mypy-extensions==1.1.0 # via mypy -nodeenv==1.8.0 +nodeenv==1.9.1 # via pyright -nox==2023.4.22 -packaging==23.2 +nox==2025.11.12 +packaging==25.0 + # via dependency-groups # via nox # via pytest pathspec==0.12.1 # via mypy -platformdirs==3.11.0 +platformdirs==4.4.0 # via virtualenv -pluggy==1.5.0 +pluggy==1.6.0 # via pytest -propcache==0.3.2 +propcache==0.4.1 # via aiohttp # via yarl -pydantic==2.11.9 +pydantic==2.12.5 # via spitch -pydantic-core==2.33.2 +pydantic-core==2.41.5 # via pydantic -pygments==2.18.0 +pygments==2.19.2 + # via pytest # via rich pyright==1.1.399 -pytest==8.3.3 +pytest==8.4.2 # via pytest-asyncio # via pytest-xdist -pytest-asyncio==0.24.0 -pytest-xdist==3.7.0 -python-dateutil==2.8.2 +pytest-asyncio==1.2.0 +pytest-xdist==3.8.0 +python-dateutil==2.9.0.post0 # via time-machine -pytz==2023.3.post1 - # via dirty-equals respx==0.22.0 -rich==13.7.1 -ruff==0.9.4 -setuptools==68.2.2 - # via nodeenv -six==1.16.0 +rich==14.2.0 +ruff==0.14.7 +six==1.17.0 # via python-dateutil -sniffio==1.3.0 - # via anyio - # via httpx +sniffio==1.3.1 # via spitch -time-machine==2.9.0 -tomli==2.0.2 +time-machine==2.19.0 +tomli==2.3.0 + # via dependency-groups # via mypy + # via nox # via pytest -typing-extensions==4.12.2 +typing-extensions==4.15.0 + # via aiosignal # via anyio + # via exceptiongroup # via multidict # via mypy # via pydantic # via pydantic-core # via pyright + # via pytest-asyncio # via spitch # via typing-inspection -typing-inspection==0.4.1 + # via virtualenv +typing-inspection==0.4.2 # via pydantic -virtualenv==20.24.5 +virtualenv==20.35.4 # via nox -yarl==1.20.1 +yarl==1.22.0 # via aiohttp -zipp==3.17.0 +zipp==3.23.0 # via importlib-metadata diff --git a/requirements.lock b/requirements.lock index 5d9c3fc..7d1ca08 100644 --- a/requirements.lock +++ b/requirements.lock @@ -12,28 +12,28 @@ -e file:. aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.12.13 +aiohttp==3.13.2 # via httpx-aiohttp # via spitch -aiosignal==1.3.2 +aiosignal==1.4.0 # via aiohttp -annotated-types==0.6.0 +annotated-types==0.7.0 # via pydantic -anyio==4.4.0 +anyio==4.12.0 # via httpx # via spitch async-timeout==5.0.1 # via aiohttp -attrs==25.3.0 +attrs==25.4.0 # via aiohttp -certifi==2023.7.22 +certifi==2025.11.12 # via httpcore # via httpx -distro==1.8.0 +distro==1.9.0 # via spitch -exceptiongroup==1.2.2 +exceptiongroup==1.3.1 # via anyio -frozenlist==1.7.0 +frozenlist==1.8.0 # via aiohttp # via aiosignal h11==0.16.0 @@ -45,26 +45,26 @@ httpx==0.27.2 # via spitch httpx-aiohttp==0.1.9 # via spitch -idna==3.4 +idna==3.11 # via anyio # via httpx # via yarl -multidict==6.5.0 +multidict==6.7.0 # via aiohttp # via yarl -propcache==0.3.2 +propcache==0.4.1 # via aiohttp # via yarl pydantic==2.12.5 # via spitch pydantic-core==2.41.5 # via pydantic -sniffio==1.3.0 - # via anyio - # via httpx +sniffio==1.3.1 # via spitch typing-extensions==4.15.0 + # via aiosignal # via anyio + # via exceptiongroup # via multidict # via pydantic # via pydantic-core @@ -72,5 +72,5 @@ typing-extensions==4.15.0 # via typing-inspection typing-inspection==0.4.2 # via pydantic -yarl==1.20.0 +yarl==1.22.0 # via aiohttp From e74350e037dc2136577aa758e493e0261ce70166 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 05:20:01 +0000 Subject: [PATCH 04/17] chore(docs): use environment variables for authentication in code snippets --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 45e7ef0..944ff82 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,7 @@ pip install spitch[aiohttp] Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: ```python +import os import asyncio from spitch import DefaultAioHttpClient from spitch import AsyncSpitch @@ -92,7 +93,7 @@ from spitch import AsyncSpitch async def main() -> None: async with AsyncSpitch( - api_key="My API Key", + api_key=os.environ.get("SPITCH_API_KEY"), # This is the default and can be omitted http_client=DefaultAioHttpClient(), ) as client: response = await client.speech.generate( From 49b39447feb57724b18dbabd08da07ff6fe10164 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 9 Dec 2025 04:25:03 +0000 Subject: [PATCH 05/17] fix(types): allow pyright to infer TypedDict types within SequenceNotStr --- src/spitch/_types.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/spitch/_types.py b/src/spitch/_types.py index c3dccf9..fcc62ee 100644 --- a/src/spitch/_types.py +++ b/src/spitch/_types.py @@ -245,6 +245,9 @@ class HttpxSendArgs(TypedDict, total=False): if TYPE_CHECKING: # This works because str.__contains__ does not accept object (either in typeshed or at runtime) # https://github.com/hauntsaninja/useful_types/blob/5e9710f3875107d068e7679fd7fec9cfab0eff3b/useful_types/__init__.py#L285 + # + # Note: index() and count() methods are intentionally omitted to allow pyright to properly + # infer TypedDict types when dict literals are used in lists assigned to SequenceNotStr. class SequenceNotStr(Protocol[_T_co]): @overload def __getitem__(self, index: SupportsIndex, /) -> _T_co: ... @@ -253,8 +256,6 @@ def __getitem__(self, index: slice, /) -> Sequence[_T_co]: ... def __contains__(self, value: object, /) -> bool: ... def __len__(self) -> int: ... def __iter__(self) -> Iterator[_T_co]: ... - def index(self, value: Any, start: int = 0, stop: int = ..., /) -> int: ... - def count(self, value: Any, /) -> int: ... def __reversed__(self) -> Iterator[_T_co]: ... else: # just point this to a normal `Sequence` at runtime to avoid having to special case From 5706a112946f0c294388455c32cf5c1e1bad270a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 9 Dec 2025 04:27:14 +0000 Subject: [PATCH 06/17] chore: add missing docstrings --- src/spitch/types/diacritics.py | 9 +++++++++ src/spitch/types/file.py | 8 ++++++++ src/spitch/types/file_download_response.py | 8 ++++++++ src/spitch/types/file_usage.py | 10 ++++++++++ src/spitch/types/files.py | 7 +++++++ src/spitch/types/job.py | 2 ++ src/spitch/types/jobs.py | 7 +++++++ src/spitch/types/translation.py | 8 ++++++++ 8 files changed, 59 insertions(+) diff --git a/src/spitch/types/diacritics.py b/src/spitch/types/diacritics.py index e9b1e07..0f3a044 100644 --- a/src/spitch/types/diacritics.py +++ b/src/spitch/types/diacritics.py @@ -6,6 +6,15 @@ class Diacritics(BaseModel): + """ + Response model for text diacritization requests. + + Attributes: + request_id: Unique identifier for the diacritization request + text: Text with added diacritical marks for proper pronunciation + language: Language code for the diacritized text + """ + request_id: str text: str diff --git a/src/spitch/types/file.py b/src/spitch/types/file.py index 86a1ab2..39e03e5 100644 --- a/src/spitch/types/file.py +++ b/src/spitch/types/file.py @@ -10,6 +10,14 @@ class File(BaseModel): + """ + description of a file. + Attributes: + file_id: unique identifier for the file. + status: status of the file, `processing` or `ready` + original_name: original name of the file. If the file was uploaded via API + """ + category: Optional[str] = None content_type: Optional[str] = None diff --git a/src/spitch/types/file_download_response.py b/src/spitch/types/file_download_response.py index 734ab58..c9e58d0 100644 --- a/src/spitch/types/file_download_response.py +++ b/src/spitch/types/file_download_response.py @@ -8,6 +8,14 @@ class FileDownloadResponse(BaseModel): + """Response model for file download URLs. + + Attributes: + file_id: Unique identifier for the file + url: Pre-signed download URL + expires_at: When the download URL expires + """ + expires_at: datetime file_id: str diff --git a/src/spitch/types/file_usage.py b/src/spitch/types/file_usage.py index 588b779..93cb866 100644 --- a/src/spitch/types/file_usage.py +++ b/src/spitch/types/file_usage.py @@ -6,6 +6,16 @@ class FileUsage(BaseModel): + """Storage usage information for an organization. + + Attributes: + total: Human-readable total storage limit + used: Human-readable used storage amount + total_bytes: Total storage limit in bytes + used_bytes: Used storage amount in bytes + num_files: Number of files stored + """ + num_files: int total: str diff --git a/src/spitch/types/files.py b/src/spitch/types/files.py index 1a27afd..6328733 100644 --- a/src/spitch/types/files.py +++ b/src/spitch/types/files.py @@ -9,6 +9,13 @@ class Files(BaseModel): + """Response model for listing files. + + Attributes: + items: List of file metadata responses + next_cursor: Optional cursor for pagination to get next page of results + """ + items: List[File] next_cursor: Optional[str] = None diff --git a/src/spitch/types/job.py b/src/spitch/types/job.py index 6b7ff69..da92372 100644 --- a/src/spitch/types/job.py +++ b/src/spitch/types/job.py @@ -8,6 +8,8 @@ class Job(BaseModel): + """Metadata model for job metadata.""" + created_by: str due_date: datetime diff --git a/src/spitch/types/jobs.py b/src/spitch/types/jobs.py index 83f5731..7df3100 100644 --- a/src/spitch/types/jobs.py +++ b/src/spitch/types/jobs.py @@ -9,6 +9,13 @@ class Jobs(BaseModel): + """Response model for listing jobs. + + Attributes: + items: list of job metadata responses + next_cursor: Optional cursor for pagination to get next page of results + """ + items: List[Job] next_cursor: Optional[str] = None diff --git a/src/spitch/types/translation.py b/src/spitch/types/translation.py index 47809c8..f5588ac 100644 --- a/src/spitch/types/translation.py +++ b/src/spitch/types/translation.py @@ -9,6 +9,14 @@ class Translation(BaseModel): + """Translation result model. + + Attributes: + request_id (UUID): Unique ID for this request. + text: translated text. + due_date: used when model is `human`. the date you can expect the translation to be delivered + """ + request_id: str text: str From d52ec358ed429f302ff0c035d1b8b94cbe24e3e0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 04:17:43 +0000 Subject: [PATCH 07/17] chore(internal): add missing files argument to base client --- src/spitch/_base_client.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/spitch/_base_client.py b/src/spitch/_base_client.py index 09bf821..c110dc0 100644 --- a/src/spitch/_base_client.py +++ b/src/spitch/_base_client.py @@ -1247,9 +1247,12 @@ def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + opts = FinalRequestOptions.construct( + method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + ) return self.request(cast_to, opts) def put( @@ -1771,9 +1774,12 @@ async def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + opts = FinalRequestOptions.construct( + method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + ) return await self.request(cast_to, opts) async def put( From 8b385dffc6d34d7cbda6ba197d08637c98a6af37 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 05:47:14 +0000 Subject: [PATCH 08/17] chore: speedup initial import --- src/spitch/_client.py | 229 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 185 insertions(+), 44 deletions(-) diff --git a/src/spitch/_client.py b/src/spitch/_client.py index e8fa1dc..4e5d00d 100644 --- a/src/spitch/_client.py +++ b/src/spitch/_client.py @@ -3,7 +3,7 @@ from __future__ import annotations import os -from typing import Any, Mapping +from typing import TYPE_CHECKING, Any, Mapping from typing_extensions import Self, override import httpx @@ -20,8 +20,8 @@ not_given, ) from ._utils import is_given, get_async_library +from ._compat import cached_property from ._version import __version__ -from .resources import jobs, text, files, speech from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import SpitchError, APIStatusError from ._base_client import ( @@ -30,6 +30,13 @@ AsyncAPIClient, ) +if TYPE_CHECKING: + from .resources import jobs, text, files, speech + from .resources.jobs import JobsResource, AsyncJobsResource + from .resources.text import TextResource, AsyncTextResource + from .resources.files import FilesResource, AsyncFilesResource + from .resources.speech import SpeechResource, AsyncSpeechResource + __all__ = [ "Timeout", "Transport", @@ -44,13 +51,6 @@ class Spitch(SyncAPIClient): - speech: speech.SpeechResource - text: text.TextResource - files: files.FilesResource - jobs: jobs.JobsResource - with_raw_response: SpitchWithRawResponse - with_streaming_response: SpitchWithStreamedResponse - # client options api_key: str @@ -105,12 +105,38 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.speech = speech.SpeechResource(self) - self.text = text.TextResource(self) - self.files = files.FilesResource(self) - self.jobs = jobs.JobsResource(self) - self.with_raw_response = SpitchWithRawResponse(self) - self.with_streaming_response = SpitchWithStreamedResponse(self) + @cached_property + def speech(self) -> SpeechResource: + """All speech-focused APIs (TTS and STT)""" + from .resources.speech import SpeechResource + + return SpeechResource(self) + + @cached_property + def text(self) -> TextResource: + from .resources.text import TextResource + + return TextResource(self) + + @cached_property + def files(self) -> FilesResource: + from .resources.files import FilesResource + + return FilesResource(self) + + @cached_property + def jobs(self) -> JobsResource: + from .resources.jobs import JobsResource + + return JobsResource(self) + + @cached_property + def with_raw_response(self) -> SpitchWithRawResponse: + return SpitchWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> SpitchWithStreamedResponse: + return SpitchWithStreamedResponse(self) @property @override @@ -218,13 +244,6 @@ def _make_status_error( class AsyncSpitch(AsyncAPIClient): - speech: speech.AsyncSpeechResource - text: text.AsyncTextResource - files: files.AsyncFilesResource - jobs: jobs.AsyncJobsResource - with_raw_response: AsyncSpitchWithRawResponse - with_streaming_response: AsyncSpitchWithStreamedResponse - # client options api_key: str @@ -279,12 +298,38 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.speech = speech.AsyncSpeechResource(self) - self.text = text.AsyncTextResource(self) - self.files = files.AsyncFilesResource(self) - self.jobs = jobs.AsyncJobsResource(self) - self.with_raw_response = AsyncSpitchWithRawResponse(self) - self.with_streaming_response = AsyncSpitchWithStreamedResponse(self) + @cached_property + def speech(self) -> AsyncSpeechResource: + """All speech-focused APIs (TTS and STT)""" + from .resources.speech import AsyncSpeechResource + + return AsyncSpeechResource(self) + + @cached_property + def text(self) -> AsyncTextResource: + from .resources.text import AsyncTextResource + + return AsyncTextResource(self) + + @cached_property + def files(self) -> AsyncFilesResource: + from .resources.files import AsyncFilesResource + + return AsyncFilesResource(self) + + @cached_property + def jobs(self) -> AsyncJobsResource: + from .resources.jobs import AsyncJobsResource + + return AsyncJobsResource(self) + + @cached_property + def with_raw_response(self) -> AsyncSpitchWithRawResponse: + return AsyncSpitchWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncSpitchWithStreamedResponse: + return AsyncSpitchWithStreamedResponse(self) @property @override @@ -392,35 +437,131 @@ def _make_status_error( class SpitchWithRawResponse: + _client: Spitch + def __init__(self, client: Spitch) -> None: - self.speech = speech.SpeechResourceWithRawResponse(client.speech) - self.text = text.TextResourceWithRawResponse(client.text) - self.files = files.FilesResourceWithRawResponse(client.files) - self.jobs = jobs.JobsResourceWithRawResponse(client.jobs) + self._client = client + + @cached_property + def speech(self) -> speech.SpeechResourceWithRawResponse: + """All speech-focused APIs (TTS and STT)""" + from .resources.speech import SpeechResourceWithRawResponse + + return SpeechResourceWithRawResponse(self._client.speech) + + @cached_property + def text(self) -> text.TextResourceWithRawResponse: + from .resources.text import TextResourceWithRawResponse + + return TextResourceWithRawResponse(self._client.text) + + @cached_property + def files(self) -> files.FilesResourceWithRawResponse: + from .resources.files import FilesResourceWithRawResponse + + return FilesResourceWithRawResponse(self._client.files) + + @cached_property + def jobs(self) -> jobs.JobsResourceWithRawResponse: + from .resources.jobs import JobsResourceWithRawResponse + + return JobsResourceWithRawResponse(self._client.jobs) class AsyncSpitchWithRawResponse: + _client: AsyncSpitch + def __init__(self, client: AsyncSpitch) -> None: - self.speech = speech.AsyncSpeechResourceWithRawResponse(client.speech) - self.text = text.AsyncTextResourceWithRawResponse(client.text) - self.files = files.AsyncFilesResourceWithRawResponse(client.files) - self.jobs = jobs.AsyncJobsResourceWithRawResponse(client.jobs) + self._client = client + + @cached_property + def speech(self) -> speech.AsyncSpeechResourceWithRawResponse: + """All speech-focused APIs (TTS and STT)""" + from .resources.speech import AsyncSpeechResourceWithRawResponse + + return AsyncSpeechResourceWithRawResponse(self._client.speech) + + @cached_property + def text(self) -> text.AsyncTextResourceWithRawResponse: + from .resources.text import AsyncTextResourceWithRawResponse + + return AsyncTextResourceWithRawResponse(self._client.text) + + @cached_property + def files(self) -> files.AsyncFilesResourceWithRawResponse: + from .resources.files import AsyncFilesResourceWithRawResponse + + return AsyncFilesResourceWithRawResponse(self._client.files) + + @cached_property + def jobs(self) -> jobs.AsyncJobsResourceWithRawResponse: + from .resources.jobs import AsyncJobsResourceWithRawResponse + + return AsyncJobsResourceWithRawResponse(self._client.jobs) class SpitchWithStreamedResponse: + _client: Spitch + def __init__(self, client: Spitch) -> None: - self.speech = speech.SpeechResourceWithStreamingResponse(client.speech) - self.text = text.TextResourceWithStreamingResponse(client.text) - self.files = files.FilesResourceWithStreamingResponse(client.files) - self.jobs = jobs.JobsResourceWithStreamingResponse(client.jobs) + self._client = client + + @cached_property + def speech(self) -> speech.SpeechResourceWithStreamingResponse: + """All speech-focused APIs (TTS and STT)""" + from .resources.speech import SpeechResourceWithStreamingResponse + + return SpeechResourceWithStreamingResponse(self._client.speech) + + @cached_property + def text(self) -> text.TextResourceWithStreamingResponse: + from .resources.text import TextResourceWithStreamingResponse + + return TextResourceWithStreamingResponse(self._client.text) + + @cached_property + def files(self) -> files.FilesResourceWithStreamingResponse: + from .resources.files import FilesResourceWithStreamingResponse + + return FilesResourceWithStreamingResponse(self._client.files) + + @cached_property + def jobs(self) -> jobs.JobsResourceWithStreamingResponse: + from .resources.jobs import JobsResourceWithStreamingResponse + + return JobsResourceWithStreamingResponse(self._client.jobs) class AsyncSpitchWithStreamedResponse: + _client: AsyncSpitch + def __init__(self, client: AsyncSpitch) -> None: - self.speech = speech.AsyncSpeechResourceWithStreamingResponse(client.speech) - self.text = text.AsyncTextResourceWithStreamingResponse(client.text) - self.files = files.AsyncFilesResourceWithStreamingResponse(client.files) - self.jobs = jobs.AsyncJobsResourceWithStreamingResponse(client.jobs) + self._client = client + + @cached_property + def speech(self) -> speech.AsyncSpeechResourceWithStreamingResponse: + """All speech-focused APIs (TTS and STT)""" + from .resources.speech import AsyncSpeechResourceWithStreamingResponse + + return AsyncSpeechResourceWithStreamingResponse(self._client.speech) + + @cached_property + def text(self) -> text.AsyncTextResourceWithStreamingResponse: + from .resources.text import AsyncTextResourceWithStreamingResponse + + return AsyncTextResourceWithStreamingResponse(self._client.text) + + @cached_property + def files(self) -> files.AsyncFilesResourceWithStreamingResponse: + from .resources.files import AsyncFilesResourceWithStreamingResponse + + return AsyncFilesResourceWithStreamingResponse(self._client.files) + + @cached_property + def jobs(self) -> jobs.AsyncJobsResourceWithStreamingResponse: + from .resources.jobs import AsyncJobsResourceWithStreamingResponse + + return AsyncJobsResourceWithStreamingResponse(self._client.jobs) Client = Spitch From 7c7152096acff1ab2777e88ea6edf8e74762a366 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 05:49:05 +0000 Subject: [PATCH 09/17] feat(files): add support for string alternative to file upload type --- src/spitch/resources/speech.py | 6 +++--- src/spitch/types/speech_transcribe_params.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/spitch/resources/speech.py b/src/spitch/resources/speech.py index 970a876..d16308e 100644 --- a/src/spitch/resources/speech.py +++ b/src/spitch/resources/speech.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Mapping, Optional, cast +from typing import Union, Mapping, Optional, cast from typing_extensions import Literal import httpx @@ -127,7 +127,7 @@ def transcribe( self, *, language: Literal["yo", "en", "ha", "ig", "am"], - content: Optional[FileTypes] | Omit = omit, + content: Union[FileTypes, str, None] | Omit = omit, model: Optional[Literal["mansa_v1", "legacy", "human"]] | Omit = omit, special_words: Optional[str] | Omit = omit, timestamp: Optional[Literal["sentence", "word", "none"]] | Omit = omit, @@ -272,7 +272,7 @@ async def transcribe( self, *, language: Literal["yo", "en", "ha", "ig", "am"], - content: Optional[FileTypes] | Omit = omit, + content: Union[FileTypes, str, None] | Omit = omit, model: Optional[Literal["mansa_v1", "legacy", "human"]] | Omit = omit, special_words: Optional[str] | Omit = omit, timestamp: Optional[Literal["sentence", "word", "none"]] | Omit = omit, diff --git a/src/spitch/types/speech_transcribe_params.py b/src/spitch/types/speech_transcribe_params.py index 4476699..8909a68 100644 --- a/src/spitch/types/speech_transcribe_params.py +++ b/src/spitch/types/speech_transcribe_params.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Optional +from typing import Union, Optional from typing_extensions import Literal, Required, TypedDict from .._types import FileTypes @@ -13,7 +13,7 @@ class SpeechTranscribeParams(TypedDict, total=False): language: Required[Literal["yo", "en", "ha", "ig", "am"]] - content: Optional[FileTypes] + content: Union[FileTypes, str, None] model: Optional[Literal["mansa_v1", "legacy", "human"]] From d6681dc12cb59df8843b6adbe09095efde66548b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 18 Dec 2025 06:29:44 +0000 Subject: [PATCH 10/17] fix: use async_to_httpx_files in patch method --- src/spitch/_base_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spitch/_base_client.py b/src/spitch/_base_client.py index c110dc0..3cd7c65 100644 --- a/src/spitch/_base_client.py +++ b/src/spitch/_base_client.py @@ -1778,7 +1778,7 @@ async def patch( options: RequestOptions = {}, ) -> ResponseT: opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + method="patch", url=path, json_data=body, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts) From 697bfd03e8bc2c855e36fbfcc23f6ecf7d740652 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 05:42:27 +0000 Subject: [PATCH 11/17] chore(internal): add `--fix` argument to lint script --- scripts/lint | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/lint b/scripts/lint index 06ef5e3..b0778e2 100755 --- a/scripts/lint +++ b/scripts/lint @@ -4,8 +4,13 @@ set -e cd "$(dirname "$0")/.." -echo "==> Running lints" -rye run lint +if [ "$1" = "--fix" ]; then + echo "==> Running lints with --fix" + rye run fix:ruff +else + echo "==> Running lints" + rye run lint +fi echo "==> Making sure it imports" rye run python -c 'import spitch' From d11c2988b89fde8ef890fe072869caf1692fe56a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 05:14:30 +0000 Subject: [PATCH 12/17] chore(internal): codegen related update --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index b0faa66..ff140ee 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2025 Spitch + Copyright 2026 Spitch Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 0c96451d56e5897221e1a5e628c557f8d47df911 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 07:21:58 +0000 Subject: [PATCH 13/17] feat(client): add support for binary request streaming --- src/spitch/_base_client.py | 145 +++++++++++++++++++++++++--- src/spitch/_models.py | 17 +++- src/spitch/_types.py | 9 ++ tests/test_client.py | 187 ++++++++++++++++++++++++++++++++++++- 4 files changed, 344 insertions(+), 14 deletions(-) diff --git a/src/spitch/_base_client.py b/src/spitch/_base_client.py index 3cd7c65..d4d25b9 100644 --- a/src/spitch/_base_client.py +++ b/src/spitch/_base_client.py @@ -9,6 +9,7 @@ import inspect import logging import platform +import warnings import email.utils from types import TracebackType from random import random @@ -51,9 +52,11 @@ ResponseT, AnyMapping, PostParser, + BinaryTypes, RequestFiles, HttpxSendArgs, RequestOptions, + AsyncBinaryTypes, HttpxRequestFiles, ModelBuilderProtocol, not_given, @@ -477,8 +480,19 @@ def _build_request( retries_taken: int = 0, ) -> httpx.Request: if log.isEnabledFor(logging.DEBUG): - log.debug("Request options: %s", model_dump(options, exclude_unset=True)) - + log.debug( + "Request options: %s", + model_dump( + options, + exclude_unset=True, + # Pydantic v1 can't dump every type we support in content, so we exclude it for now. + exclude={ + "content", + } + if PYDANTIC_V1 + else {}, + ), + ) kwargs: dict[str, Any] = {} json_data = options.json_data @@ -532,7 +546,13 @@ def _build_request( is_body_allowed = options.method.lower() != "get" if is_body_allowed: - if isinstance(json_data, bytes): + if options.content is not None and json_data is not None: + raise TypeError("Passing both `content` and `json_data` is not supported") + if options.content is not None and files is not None: + raise TypeError("Passing both `content` and `files` is not supported") + if options.content is not None: + kwargs["content"] = options.content + elif isinstance(json_data, bytes): kwargs["content"] = json_data else: kwargs["json"] = json_data if is_given(json_data) else None @@ -1194,6 +1214,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: Literal[False] = False, @@ -1206,6 +1227,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: Literal[True], @@ -1219,6 +1241,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: bool, @@ -1231,13 +1254,25 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: bool = False, stream_cls: type[_StreamT] | None = None, ) -> ResponseT | _StreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=to_httpx_files(files), **options + method="post", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) @@ -1247,11 +1282,23 @@ def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + method="patch", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return self.request(cast_to, opts) @@ -1261,11 +1308,23 @@ def put( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=to_httpx_files(files), **options + method="put", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return self.request(cast_to, opts) @@ -1275,9 +1334,19 @@ def delete( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) return self.request(cast_to, opts) def get_api_list( @@ -1721,6 +1790,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: Literal[False] = False, @@ -1733,6 +1803,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: Literal[True], @@ -1746,6 +1817,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: bool, @@ -1758,13 +1830,25 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: bool = False, stream_cls: type[_AsyncStreamT] | None = None, ) -> ResponseT | _AsyncStreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="post", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) @@ -1774,11 +1858,28 @@ async def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="patch", + url=path, + json_data=body, + content=content, + files=await async_to_httpx_files(files), + **options, ) return await self.request(cast_to, opts) @@ -1788,11 +1889,23 @@ async def put( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="put", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts) @@ -1802,9 +1915,19 @@ async def delete( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) return await self.request(cast_to, opts) def get_api_list( diff --git a/src/spitch/_models.py b/src/spitch/_models.py index 84e531c..99e58f2 100644 --- a/src/spitch/_models.py +++ b/src/spitch/_models.py @@ -3,7 +3,20 @@ import os import inspect import weakref -from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast +from typing import ( + IO, + TYPE_CHECKING, + Any, + Type, + Union, + Generic, + TypeVar, + Callable, + Iterable, + Optional, + AsyncIterable, + cast, +) from datetime import date, datetime from typing_extensions import ( List, @@ -783,6 +796,7 @@ class FinalRequestOptionsInput(TypedDict, total=False): timeout: float | Timeout | None files: HttpxRequestFiles | None idempotency_key: str + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] json_data: Body extra_json: AnyMapping follow_redirects: bool @@ -801,6 +815,7 @@ class FinalRequestOptions(pydantic.BaseModel): post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() follow_redirects: Union[bool, None] = None + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] = None # It should be noted that we cannot use `json` here as that would override # a BaseModel method in an incompatible fashion. json_data: Union[Body, None] = None diff --git a/src/spitch/_types.py b/src/spitch/_types.py index fcc62ee..74716c3 100644 --- a/src/spitch/_types.py +++ b/src/spitch/_types.py @@ -13,9 +13,11 @@ Mapping, TypeVar, Callable, + Iterable, Iterator, Optional, Sequence, + AsyncIterable, ) from typing_extensions import ( Set, @@ -56,6 +58,13 @@ else: Base64FileInput = Union[IO[bytes], PathLike] FileContent = Union[IO[bytes], bytes, PathLike] # PathLike is not subscriptable in Python 3.8. + + +# Used for sending raw binary data / streaming data in request bodies +# e.g. for file uploads without multipart encoding +BinaryTypes = Union[bytes, bytearray, IO[bytes], Iterable[bytes]] +AsyncBinaryTypes = Union[bytes, bytearray, IO[bytes], AsyncIterable[bytes]] + FileTypes = Union[ # file (or bytes) FileContent, diff --git a/tests/test_client.py b/tests/test_client.py index df13bc3..8b9b263 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -8,10 +8,11 @@ import json import asyncio import inspect +import dataclasses import tracemalloc -from typing import Any, Union, cast +from typing import Any, Union, TypeVar, Callable, Iterable, Iterator, Optional, Coroutine, cast from unittest import mock -from typing_extensions import Literal +from typing_extensions import Literal, AsyncIterator, override import httpx import pytest @@ -36,6 +37,7 @@ from .utils import update_env +T = TypeVar("T") base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") api_key = "My API Key" @@ -50,6 +52,57 @@ def _low_retry_timeout(*_args: Any, **_kwargs: Any) -> float: return 0.1 +def mirror_request_content(request: httpx.Request) -> httpx.Response: + return httpx.Response(200, content=request.content) + + +# note: we can't use the httpx.MockTransport class as it consumes the request +# body itself, which means we can't test that the body is read lazily +class MockTransport(httpx.BaseTransport, httpx.AsyncBaseTransport): + def __init__( + self, + handler: Callable[[httpx.Request], httpx.Response] + | Callable[[httpx.Request], Coroutine[Any, Any, httpx.Response]], + ) -> None: + self.handler = handler + + @override + def handle_request( + self, + request: httpx.Request, + ) -> httpx.Response: + assert not inspect.iscoroutinefunction(self.handler), "handler must not be a coroutine function" + assert inspect.isfunction(self.handler), "handler must be a function" + return self.handler(request) + + @override + async def handle_async_request( + self, + request: httpx.Request, + ) -> httpx.Response: + assert inspect.iscoroutinefunction(self.handler), "handler must be a coroutine function" + return await self.handler(request) + + +@dataclasses.dataclass +class Counter: + value: int = 0 + + +def _make_sync_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> Iterator[T]: + for item in iterable: + if counter: + counter.value += 1 + yield item + + +async def _make_async_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> AsyncIterator[T]: + for item in iterable: + if counter: + counter.value += 1 + yield item + + def _get_open_connections(client: Spitch | AsyncSpitch) -> int: transport = client._client._transport assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport) @@ -500,6 +553,70 @@ def test_multipart_repeating_array(self, client: Spitch) -> None: b"", ] + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload(self, respx_mock: MockRouter, client: Spitch) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + response = client.post( + "/upload", + content=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + + def test_binary_content_upload_with_iterator(self) -> None: + file_content = b"Hello, this is a test file." + counter = Counter() + iterator = _make_sync_iterator([file_content], counter=counter) + + def mock_handler(request: httpx.Request) -> httpx.Response: + assert counter.value == 0, "the request body should not have been read" + return httpx.Response(200, content=request.read()) + + with Spitch( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.Client(transport=MockTransport(handler=mock_handler)), + ) as client: + response = client.post( + "/upload", + content=iterator, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + assert counter.value == 1 + + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload_with_body_is_deprecated(self, respx_mock: MockRouter, client: Spitch) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + with pytest.deprecated_call( + match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead." + ): + response = client.post( + "/upload", + body=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + @pytest.mark.respx(base_url=base_url) def test_basic_union_response(self, respx_mock: MockRouter, client: Spitch) -> None: class Model1(BaseModel): @@ -1323,6 +1440,72 @@ def test_multipart_repeating_array(self, async_client: AsyncSpitch) -> None: b"", ] + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload(self, respx_mock: MockRouter, async_client: AsyncSpitch) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + response = await async_client.post( + "/upload", + content=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + + async def test_binary_content_upload_with_asynciterator(self) -> None: + file_content = b"Hello, this is a test file." + counter = Counter() + iterator = _make_async_iterator([file_content], counter=counter) + + async def mock_handler(request: httpx.Request) -> httpx.Response: + assert counter.value == 0, "the request body should not have been read" + return httpx.Response(200, content=await request.aread()) + + async with AsyncSpitch( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.AsyncClient(transport=MockTransport(handler=mock_handler)), + ) as client: + response = await client.post( + "/upload", + content=iterator, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + assert counter.value == 1 + + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload_with_body_is_deprecated( + self, respx_mock: MockRouter, async_client: AsyncSpitch + ) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + with pytest.deprecated_call( + match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead." + ): + response = await async_client.post( + "/upload", + body=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + @pytest.mark.respx(base_url=base_url) async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncSpitch) -> None: class Model1(BaseModel): From afdc7e653e6f39b32558d16ab286f83d5eda385b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 17 Jan 2026 02:23:09 +0000 Subject: [PATCH 14/17] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index d0ce107..7741699 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 12 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/babs-technologies%2Fspitch-b384c3db1a21cd96c342e397f623e66793d5da64a726644609f7803ef6b5886a.yml openapi_spec_hash: eab054bdfec2c83b963ea3f120bf4109 -config_hash: 5945e27bb7f451aa23c0fe256740d398 +config_hash: c4f2f3efcf1cb1208827064ebdb2bde0 From 9dfcb509c7bed5382e2de4663838fc0583774bc2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 17 Jan 2026 05:47:06 +0000 Subject: [PATCH 15/17] chore(internal): update `actions/checkout` version --- .github/workflows/ci.yml | 6 +++--- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/release-doctor.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 54509c1..b7887bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/spitch-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | @@ -44,7 +44,7 @@ jobs: id-token: write runs-on: ${{ github.repository == 'stainless-sdks/spitch-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | @@ -81,7 +81,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/spitch-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index cf9aa94..a87cf3f 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index 3405315..b12aede 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -12,7 +12,7 @@ jobs: if: github.repository == 'spi-tch/spitch-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Check release environment run: | From 12f75eff7f034196f0f453dfb4b4c800234d9198 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 16:59:10 +0000 Subject: [PATCH 16/17] feat(api): manual updates --- .stats.yml | 8 +- api.md | 23 +- src/spitch/_client.py | 39 +-- src/spitch/resources/__init__.py | 14 - src/spitch/resources/files.py | 145 ++-------- src/spitch/resources/jobs.py | 271 ------------------ src/spitch/resources/speech.py | 104 +++++-- src/spitch/resources/text.py | 47 ++- src/spitch/types/__init__.py | 8 +- src/spitch/types/diacritics.py | 9 - src/spitch/types/file.py | 13 +- src/spitch/types/file_delete_response.py | 11 + src/spitch/types/file_download_response.py | 23 -- src/spitch/types/file_list_params.py | 4 +- src/spitch/types/file_usage.py | 27 +- src/spitch/types/files.py | 7 +- src/spitch/types/job.py | 21 -- src/spitch/types/job_list_params.py | 16 -- src/spitch/types/jobs.py | 21 -- src/spitch/types/segment.py | 21 ++ src/spitch/types/speech_generate_params.py | 10 +- src/spitch/types/speech_transcribe_params.py | 9 +- .../types/speech_transcribe_response.py | 23 ++ src/spitch/types/text_tone_mark_params.py | 2 +- src/spitch/types/text_translate_params.py | 13 +- src/spitch/types/transcription.py | 23 -- src/spitch/types/translation.py | 13 - tests/api_resources/test_files.py | 36 +-- tests/api_resources/test_jobs.py | 169 ----------- tests/api_resources/test_speech.py | 28 +- tests/api_resources/test_text.py | 26 +- 31 files changed, 250 insertions(+), 934 deletions(-) delete mode 100644 src/spitch/resources/jobs.py create mode 100644 src/spitch/types/file_delete_response.py delete mode 100644 src/spitch/types/file_download_response.py delete mode 100644 src/spitch/types/job.py delete mode 100644 src/spitch/types/job_list_params.py delete mode 100644 src/spitch/types/jobs.py create mode 100644 src/spitch/types/segment.py create mode 100644 src/spitch/types/speech_transcribe_response.py delete mode 100644 src/spitch/types/transcription.py delete mode 100644 tests/api_resources/test_jobs.py diff --git a/.stats.yml b/.stats.yml index 7741699..03042bb 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 12 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/babs-technologies%2Fspitch-b384c3db1a21cd96c342e397f623e66793d5da64a726644609f7803ef6b5886a.yml -openapi_spec_hash: eab054bdfec2c83b963ea3f120bf4109 -config_hash: c4f2f3efcf1cb1208827064ebdb2bde0 +configured_endpoints: 10 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/babs-technologies%2Fspitch-9caec45089fb23c139a076c7f6a62fdf1397b3384d3d450e5fcefdc840e7eafc.yml +openapi_spec_hash: 9b85c4aaecd7073df4e7e94a1887475b +config_hash: f3bd776604dfca42c23f55d275b73bd0 diff --git a/api.md b/api.md index 70dfb2c..7cb72b3 100644 --- a/api.md +++ b/api.md @@ -3,13 +3,13 @@ Types: ```python -from spitch.types import Transcription +from spitch.types import Segment, SpeechTranscribeResponse ``` Methods: - client.speech.generate(\*\*params) -> BinaryAPIResponse -- client.speech.transcribe(\*\*params) -> Transcription +- client.speech.transcribe(\*\*params) -> SpeechTranscribeResponse # Text @@ -29,27 +29,14 @@ Methods: Types: ```python -from spitch.types import File, FileUsage, Files, FileDownloadResponse +from spitch.types import File, FileUsage, Files, FileDeleteResponse ``` Methods: - client.files.list(\*\*params) -> SyncFilesCursor[File] -- client.files.delete(file_id) -> object -- client.files.download(file_id, \*\*params) -> FileDownloadResponse +- client.files.delete(file_id) -> FileDeleteResponse +- client.files.download(file_id, \*\*params) -> object - client.files.get(file_id) -> File - client.files.upload(\*\*params) -> File - client.files.usage() -> FileUsage - -# Jobs - -Types: - -```python -from spitch.types import Job, Jobs -``` - -Methods: - -- client.jobs.list(\*\*params) -> SyncFilesCursor[Job] -- client.jobs.get(job_id) -> Job diff --git a/src/spitch/_client.py b/src/spitch/_client.py index 4e5d00d..14c6822 100644 --- a/src/spitch/_client.py +++ b/src/spitch/_client.py @@ -31,8 +31,7 @@ ) if TYPE_CHECKING: - from .resources import jobs, text, files, speech - from .resources.jobs import JobsResource, AsyncJobsResource + from .resources import text, files, speech from .resources.text import TextResource, AsyncTextResource from .resources.files import FilesResource, AsyncFilesResource from .resources.speech import SpeechResource, AsyncSpeechResource @@ -124,12 +123,6 @@ def files(self) -> FilesResource: return FilesResource(self) - @cached_property - def jobs(self) -> JobsResource: - from .resources.jobs import JobsResource - - return JobsResource(self) - @cached_property def with_raw_response(self) -> SpitchWithRawResponse: return SpitchWithRawResponse(self) @@ -317,12 +310,6 @@ def files(self) -> AsyncFilesResource: return AsyncFilesResource(self) - @cached_property - def jobs(self) -> AsyncJobsResource: - from .resources.jobs import AsyncJobsResource - - return AsyncJobsResource(self) - @cached_property def with_raw_response(self) -> AsyncSpitchWithRawResponse: return AsyncSpitchWithRawResponse(self) @@ -461,12 +448,6 @@ def files(self) -> files.FilesResourceWithRawResponse: return FilesResourceWithRawResponse(self._client.files) - @cached_property - def jobs(self) -> jobs.JobsResourceWithRawResponse: - from .resources.jobs import JobsResourceWithRawResponse - - return JobsResourceWithRawResponse(self._client.jobs) - class AsyncSpitchWithRawResponse: _client: AsyncSpitch @@ -493,12 +474,6 @@ def files(self) -> files.AsyncFilesResourceWithRawResponse: return AsyncFilesResourceWithRawResponse(self._client.files) - @cached_property - def jobs(self) -> jobs.AsyncJobsResourceWithRawResponse: - from .resources.jobs import AsyncJobsResourceWithRawResponse - - return AsyncJobsResourceWithRawResponse(self._client.jobs) - class SpitchWithStreamedResponse: _client: Spitch @@ -525,12 +500,6 @@ def files(self) -> files.FilesResourceWithStreamingResponse: return FilesResourceWithStreamingResponse(self._client.files) - @cached_property - def jobs(self) -> jobs.JobsResourceWithStreamingResponse: - from .resources.jobs import JobsResourceWithStreamingResponse - - return JobsResourceWithStreamingResponse(self._client.jobs) - class AsyncSpitchWithStreamedResponse: _client: AsyncSpitch @@ -557,12 +526,6 @@ def files(self) -> files.AsyncFilesResourceWithStreamingResponse: return AsyncFilesResourceWithStreamingResponse(self._client.files) - @cached_property - def jobs(self) -> jobs.AsyncJobsResourceWithStreamingResponse: - from .resources.jobs import AsyncJobsResourceWithStreamingResponse - - return AsyncJobsResourceWithStreamingResponse(self._client.jobs) - Client = Spitch diff --git a/src/spitch/resources/__init__.py b/src/spitch/resources/__init__.py index 95209fa..ecbcde6 100644 --- a/src/spitch/resources/__init__.py +++ b/src/spitch/resources/__init__.py @@ -1,13 +1,5 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from .jobs import ( - JobsResource, - AsyncJobsResource, - JobsResourceWithRawResponse, - AsyncJobsResourceWithRawResponse, - JobsResourceWithStreamingResponse, - AsyncJobsResourceWithStreamingResponse, -) from .text import ( TextResource, AsyncTextResource, @@ -52,10 +44,4 @@ "AsyncFilesResourceWithRawResponse", "FilesResourceWithStreamingResponse", "AsyncFilesResourceWithStreamingResponse", - "JobsResource", - "AsyncJobsResource", - "JobsResourceWithRawResponse", - "AsyncJobsResourceWithRawResponse", - "JobsResourceWithStreamingResponse", - "AsyncJobsResourceWithStreamingResponse", ] diff --git a/src/spitch/resources/files.py b/src/spitch/resources/files.py index 5a02bfe..1bf894b 100644 --- a/src/spitch/resources/files.py +++ b/src/spitch/resources/files.py @@ -3,7 +3,6 @@ from __future__ import annotations from typing import Mapping, Optional, cast -from typing_extensions import Literal import httpx @@ -22,7 +21,7 @@ from ..types.file import File from .._base_client import AsyncPaginator, make_request_options from ..types.file_usage import FileUsage -from ..types.file_download_response import FileDownloadResponse +from ..types.file_delete_response import FileDeleteResponse __all__ = ["FilesResource", "AsyncFilesResource"] @@ -52,7 +51,6 @@ def list( *, cursor: Optional[str] | Omit = omit, limit: int | Omit = omit, - status: Optional[Literal["uploading", "ready"]] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -61,16 +59,7 @@ def list( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SyncFilesCursor[File]: """ - Get a paginated list of files for the authenticated organization. - - Args: identity: Authentication identity containing org_id and user_id limit: - Maximum number of files to return (max 100) status: Optional filter by file - status (processing, ready, etc.) cursor: Optional pagination cursor for getting - next page db: Database session - - Returns: ListFilesResponse: Paginated list of files with metadata - - Raises: HTTPException: 400 if cursor is invalid + Get Files Args: extra_headers: Send extra headers @@ -93,7 +82,6 @@ def list( { "cursor": cursor, "limit": limit, - "status": status, }, file_list_params.FileListParams, ), @@ -111,16 +99,9 @@ def delete( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> object: + ) -> FileDeleteResponse: """ - Delete a file and its associated S3 object. - - Args: file_id: UUID of the file to delete identity: Authentication identity - containing org_id db: Database session s3: S3 session for deleting objects - - Returns: dict: Success confirmation - - Raises: HTTPException: 404 if file not found or doesn't belong to org + Delete File Args: extra_headers: Send extra headers @@ -138,7 +119,7 @@ def delete( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=object, + cast_to=FileDeleteResponse, ) def download( @@ -152,19 +133,9 @@ def download( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> FileDownloadResponse: + ) -> object: """ - Generate a pre-signed download URL for a file. - - Args: file_id: UUID of the file to download identity: Authentication identity - containing org_id ttl: Time-to-live for the download URL in seconds (60-3600) - db: Database session s3: S3 session for generating pre-signed URLs - - Returns: FileDownloadResponse: Contains file_id, download URL, and expiration - time - - Raises: HTTPException: 404 if file not found, doesn't belong to org, or not - ready + Download File Args: extra_headers: Send extra headers @@ -186,7 +157,7 @@ def download( timeout=timeout, query=maybe_transform({"ttl": ttl}, file_download_params.FileDownloadParams), ), - cast_to=FileDownloadResponse, + cast_to=object, ) def get( @@ -201,15 +172,7 @@ def get( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> File: """ - Get metadata for a specific file. - - Args: file_id: UUID of the file to retrieve identity: Authentication identity - containing org_id db: Database session - - Returns: FileMetaResponse: File metadata including upload information - - Raises: HTTPException: 404 if file not found or doesn't belong to org - HTTPException: 500 if uploader information is corrupted + Get File Args: extra_headers: Send extra headers @@ -242,14 +205,7 @@ def upload( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> File: """ - Upload a file to the Spitch server. - - Args: file: The file to upload from the request identity: Authentication - identity containing org_id and user_id db: Database session - - Returns: FileMetaResponse: Metadata for the uploaded file - - Raises: HTTPException: 500 if upload fails + Upload a file to your storage. Args: extra_headers: Send extra headers @@ -286,15 +242,7 @@ def usage( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> FileUsage: - """ - Get storage usage statistics for the authenticated organization. - - Args: identity: Authentication identity containing org_id db: Database session - - Returns: FileUsage: Usage statistics including total/used bytes and file count - - Raises: HTTPException: 500 if unable to calculate usage - """ + """Get Usage""" return self._get( "/v1/files:usage", options=make_request_options( @@ -329,7 +277,6 @@ def list( *, cursor: Optional[str] | Omit = omit, limit: int | Omit = omit, - status: Optional[Literal["uploading", "ready"]] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -338,16 +285,7 @@ def list( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AsyncPaginator[File, AsyncFilesCursor[File]]: """ - Get a paginated list of files for the authenticated organization. - - Args: identity: Authentication identity containing org_id and user_id limit: - Maximum number of files to return (max 100) status: Optional filter by file - status (processing, ready, etc.) cursor: Optional pagination cursor for getting - next page db: Database session - - Returns: ListFilesResponse: Paginated list of files with metadata - - Raises: HTTPException: 400 if cursor is invalid + Get Files Args: extra_headers: Send extra headers @@ -370,7 +308,6 @@ def list( { "cursor": cursor, "limit": limit, - "status": status, }, file_list_params.FileListParams, ), @@ -388,16 +325,9 @@ async def delete( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> object: + ) -> FileDeleteResponse: """ - Delete a file and its associated S3 object. - - Args: file_id: UUID of the file to delete identity: Authentication identity - containing org_id db: Database session s3: S3 session for deleting objects - - Returns: dict: Success confirmation - - Raises: HTTPException: 404 if file not found or doesn't belong to org + Delete File Args: extra_headers: Send extra headers @@ -415,7 +345,7 @@ async def delete( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=object, + cast_to=FileDeleteResponse, ) async def download( @@ -429,19 +359,9 @@ async def download( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> FileDownloadResponse: + ) -> object: """ - Generate a pre-signed download URL for a file. - - Args: file_id: UUID of the file to download identity: Authentication identity - containing org_id ttl: Time-to-live for the download URL in seconds (60-3600) - db: Database session s3: S3 session for generating pre-signed URLs - - Returns: FileDownloadResponse: Contains file_id, download URL, and expiration - time - - Raises: HTTPException: 404 if file not found, doesn't belong to org, or not - ready + Download File Args: extra_headers: Send extra headers @@ -463,7 +383,7 @@ async def download( timeout=timeout, query=await async_maybe_transform({"ttl": ttl}, file_download_params.FileDownloadParams), ), - cast_to=FileDownloadResponse, + cast_to=object, ) async def get( @@ -478,15 +398,7 @@ async def get( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> File: """ - Get metadata for a specific file. - - Args: file_id: UUID of the file to retrieve identity: Authentication identity - containing org_id db: Database session - - Returns: FileMetaResponse: File metadata including upload information - - Raises: HTTPException: 404 if file not found or doesn't belong to org - HTTPException: 500 if uploader information is corrupted + Get File Args: extra_headers: Send extra headers @@ -519,14 +431,7 @@ async def upload( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> File: """ - Upload a file to the Spitch server. - - Args: file: The file to upload from the request identity: Authentication - identity containing org_id and user_id db: Database session - - Returns: FileMetaResponse: Metadata for the uploaded file - - Raises: HTTPException: 500 if upload fails + Upload a file to your storage. Args: extra_headers: Send extra headers @@ -563,15 +468,7 @@ async def usage( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> FileUsage: - """ - Get storage usage statistics for the authenticated organization. - - Args: identity: Authentication identity containing org_id db: Database session - - Returns: FileUsage: Usage statistics including total/used bytes and file count - - Raises: HTTPException: 500 if unable to calculate usage - """ + """Get Usage""" return await self._get( "/v1/files:usage", options=make_request_options( diff --git a/src/spitch/resources/jobs.py b/src/spitch/resources/jobs.py deleted file mode 100644 index 6e28035..0000000 --- a/src/spitch/resources/jobs.py +++ /dev/null @@ -1,271 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Optional -from typing_extensions import Literal - -import httpx - -from ..types import job_list_params -from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from .._utils import maybe_transform -from .._compat import cached_property -from .._resource import SyncAPIResource, AsyncAPIResource -from .._response import ( - to_raw_response_wrapper, - to_streamed_response_wrapper, - async_to_raw_response_wrapper, - async_to_streamed_response_wrapper, -) -from ..types.job import Job -from ..pagination import SyncFilesCursor, AsyncFilesCursor -from .._base_client import AsyncPaginator, make_request_options - -__all__ = ["JobsResource", "AsyncJobsResource"] - - -class JobsResource(SyncAPIResource): - @cached_property - def with_raw_response(self) -> JobsResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/spi-tch/spitch-python#accessing-raw-response-data-eg-headers - """ - return JobsResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> JobsResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/spi-tch/spitch-python#with_streaming_response - """ - return JobsResourceWithStreamingResponse(self) - - def list( - self, - *, - cursor: Optional[str] | Omit = omit, - limit: int | Omit = omit, - status: Optional[Literal["queued", "in_progress", "finished"]] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SyncFilesCursor[Job]: - """ - Get Jobs - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return self._get_api_list( - "/v1/jobs", - page=SyncFilesCursor[Job], - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=maybe_transform( - { - "cursor": cursor, - "limit": limit, - "status": status, - }, - job_list_params.JobListParams, - ), - ), - model=Job, - ) - - def get( - self, - job_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Job: - """ - Get Job - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not job_id: - raise ValueError(f"Expected a non-empty value for `job_id` but received {job_id!r}") - return self._get( - f"/v1/jobs/{job_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=Job, - ) - - -class AsyncJobsResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncJobsResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/spi-tch/spitch-python#accessing-raw-response-data-eg-headers - """ - return AsyncJobsResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncJobsResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/spi-tch/spitch-python#with_streaming_response - """ - return AsyncJobsResourceWithStreamingResponse(self) - - def list( - self, - *, - cursor: Optional[str] | Omit = omit, - limit: int | Omit = omit, - status: Optional[Literal["queued", "in_progress", "finished"]] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> AsyncPaginator[Job, AsyncFilesCursor[Job]]: - """ - Get Jobs - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return self._get_api_list( - "/v1/jobs", - page=AsyncFilesCursor[Job], - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=maybe_transform( - { - "cursor": cursor, - "limit": limit, - "status": status, - }, - job_list_params.JobListParams, - ), - ), - model=Job, - ) - - async def get( - self, - job_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Job: - """ - Get Job - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not job_id: - raise ValueError(f"Expected a non-empty value for `job_id` but received {job_id!r}") - return await self._get( - f"/v1/jobs/{job_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=Job, - ) - - -class JobsResourceWithRawResponse: - def __init__(self, jobs: JobsResource) -> None: - self._jobs = jobs - - self.list = to_raw_response_wrapper( - jobs.list, - ) - self.get = to_raw_response_wrapper( - jobs.get, - ) - - -class AsyncJobsResourceWithRawResponse: - def __init__(self, jobs: AsyncJobsResource) -> None: - self._jobs = jobs - - self.list = async_to_raw_response_wrapper( - jobs.list, - ) - self.get = async_to_raw_response_wrapper( - jobs.get, - ) - - -class JobsResourceWithStreamingResponse: - def __init__(self, jobs: JobsResource) -> None: - self._jobs = jobs - - self.list = to_streamed_response_wrapper( - jobs.list, - ) - self.get = to_streamed_response_wrapper( - jobs.get, - ) - - -class AsyncJobsResourceWithStreamingResponse: - def __init__(self, jobs: AsyncJobsResource) -> None: - self._jobs = jobs - - self.list = async_to_streamed_response_wrapper( - jobs.list, - ) - self.get = async_to_streamed_response_wrapper( - jobs.get, - ) diff --git a/src/spitch/resources/speech.py b/src/spitch/resources/speech.py index d16308e..479ff79 100644 --- a/src/spitch/resources/speech.py +++ b/src/spitch/resources/speech.py @@ -9,7 +9,7 @@ from ..types import speech_generate_params, speech_transcribe_params from .._types import Body, Omit, Query, Headers, NotGiven, FileTypes, omit, not_given -from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform +from .._utils import is_given, extract_files, maybe_transform, strip_not_given, deepcopy_minimal, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -27,7 +27,7 @@ async_to_custom_streamed_response_wrapper, ) from .._base_client import make_request_options -from ..types.transcription import Transcription +from ..types.speech_transcribe_response import SpeechTranscribeResponse __all__ = ["SpeechResource", "AsyncSpeechResource"] @@ -57,7 +57,7 @@ def with_streaming_response(self) -> SpeechResourceWithStreamingResponse: def generate( self, *, - language: Literal["yo", "en", "ha", "ig", "am"], + language: Literal["yo", "en", "ha", "ig", "am", "pcm"], text: str, voice: Literal[ "sade", @@ -83,8 +83,8 @@ def generate( "tena", "tesfaye", ], - format: Literal["wav", "mp3", "ogg_opus", "webm_opus", "flac", "pcm_s16le", "mulaw", "alaw"] | Omit = omit, - model: Optional[Literal["legacy"]] | Omit = omit, + model: Optional[str] | Omit = omit, + spitch_x_data_retention: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -92,8 +92,10 @@ def generate( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> BinaryAPIResponse: - """ - Synthesize + """Convert text to speech. + + Select a voice and use that to generate audio in any + format. Audio is retured in chunks. Args: extra_headers: Send extra headers @@ -104,7 +106,17 @@ def generate( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {"Accept": "audio/*", **(extra_headers or {})} + extra_headers = {"Accept": "audio/wav", **(extra_headers or {})} + extra_headers = { + **strip_not_given( + { + "Spitch-X-Data-Retention": ("true" if spitch_x_data_retention else "false") + if is_given(spitch_x_data_retention) + else not_given + } + ), + **(extra_headers or {}), + } return self._post( "/v1/speech", body=maybe_transform( @@ -112,7 +124,6 @@ def generate( "language": language, "text": text, "voice": voice, - "format": format, "model": model, }, speech_generate_params.SpeechGenerateParams, @@ -126,21 +137,24 @@ def generate( def transcribe( self, *, - language: Literal["yo", "en", "ha", "ig", "am"], + language: Literal["yo", "en", "ha", "ig", "am", "pcm"], content: Union[FileTypes, str, None] | Omit = omit, - model: Optional[Literal["mansa_v1", "legacy", "human"]] | Omit = omit, + model: Optional[Literal["mansa_v1", "legacy"]] | Omit = omit, special_words: Optional[str] | Omit = omit, timestamp: Optional[Literal["sentence", "word", "none"]] | Omit = omit, url: Optional[str] | Omit = omit, + spitch_x_data_retention: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Transcription: - """ - Transcribe + ) -> SpeechTranscribeResponse: + """Convert speech to text. + + Upload audio file containing speech and get back text + that represents the content of the audio file. Args: extra_headers: Send extra headers @@ -151,6 +165,16 @@ def transcribe( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = { + **strip_not_given( + { + "Spitch-X-Data-Retention": ("true" if spitch_x_data_retention else "false") + if is_given(spitch_x_data_retention) + else not_given + } + ), + **(extra_headers or {}), + } body = deepcopy_minimal( { "language": language, @@ -173,7 +197,7 @@ def transcribe( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Transcription, + cast_to=SpeechTranscribeResponse, ) @@ -202,7 +226,7 @@ def with_streaming_response(self) -> AsyncSpeechResourceWithStreamingResponse: async def generate( self, *, - language: Literal["yo", "en", "ha", "ig", "am"], + language: Literal["yo", "en", "ha", "ig", "am", "pcm"], text: str, voice: Literal[ "sade", @@ -228,8 +252,8 @@ async def generate( "tena", "tesfaye", ], - format: Literal["wav", "mp3", "ogg_opus", "webm_opus", "flac", "pcm_s16le", "mulaw", "alaw"] | Omit = omit, - model: Optional[Literal["legacy"]] | Omit = omit, + model: Optional[str] | Omit = omit, + spitch_x_data_retention: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -237,8 +261,10 @@ async def generate( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AsyncBinaryAPIResponse: - """ - Synthesize + """Convert text to speech. + + Select a voice and use that to generate audio in any + format. Audio is retured in chunks. Args: extra_headers: Send extra headers @@ -249,7 +275,17 @@ async def generate( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {"Accept": "audio/*", **(extra_headers or {})} + extra_headers = {"Accept": "audio/wav", **(extra_headers or {})} + extra_headers = { + **strip_not_given( + { + "Spitch-X-Data-Retention": ("true" if spitch_x_data_retention else "false") + if is_given(spitch_x_data_retention) + else not_given + } + ), + **(extra_headers or {}), + } return await self._post( "/v1/speech", body=await async_maybe_transform( @@ -257,7 +293,6 @@ async def generate( "language": language, "text": text, "voice": voice, - "format": format, "model": model, }, speech_generate_params.SpeechGenerateParams, @@ -271,21 +306,24 @@ async def generate( async def transcribe( self, *, - language: Literal["yo", "en", "ha", "ig", "am"], + language: Literal["yo", "en", "ha", "ig", "am", "pcm"], content: Union[FileTypes, str, None] | Omit = omit, - model: Optional[Literal["mansa_v1", "legacy", "human"]] | Omit = omit, + model: Optional[Literal["mansa_v1", "legacy"]] | Omit = omit, special_words: Optional[str] | Omit = omit, timestamp: Optional[Literal["sentence", "word", "none"]] | Omit = omit, url: Optional[str] | Omit = omit, + spitch_x_data_retention: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> Transcription: - """ - Transcribe + ) -> SpeechTranscribeResponse: + """Convert speech to text. + + Upload audio file containing speech and get back text + that represents the content of the audio file. Args: extra_headers: Send extra headers @@ -296,6 +334,16 @@ async def transcribe( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = { + **strip_not_given( + { + "Spitch-X-Data-Retention": ("true" if spitch_x_data_retention else "false") + if is_given(spitch_x_data_retention) + else not_given + } + ), + **(extra_headers or {}), + } body = deepcopy_minimal( { "language": language, @@ -318,7 +366,7 @@ async def transcribe( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Transcription, + cast_to=SpeechTranscribeResponse, ) diff --git a/src/spitch/resources/text.py b/src/spitch/resources/text.py index 2c19f1b..8d97ecc 100644 --- a/src/spitch/resources/text.py +++ b/src/spitch/resources/text.py @@ -2,13 +2,12 @@ from __future__ import annotations -from typing import Optional from typing_extensions import Literal import httpx from ..types import text_tone_mark_params, text_translate_params -from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._types import Body, Query, Headers, NotGiven, not_given from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -48,7 +47,7 @@ def with_streaming_response(self) -> TextResourceWithStreamingResponse: def tone_mark( self, *, - language: Literal["yo", "en", "ha", "ig", "am"], + language: Literal["yo", "en", "ha", "ig", "am", "pcm"], text: str, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -58,7 +57,7 @@ def tone_mark( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Diacritics: """ - Tone Mark + Add appopriate tone marks to text. Args: extra_headers: Send extra headers @@ -87,12 +86,9 @@ def tone_mark( def translate( self, *, - source: Literal["yo", "en", "ha", "ig", "am"], - target: Literal["yo", "en", "ha", "ig", "am"], - file_id: Optional[str] | Omit = omit, - instructions: Optional[str] | Omit = omit, - model: Literal["human", "auto"] | Omit = omit, - text: Optional[str] | Omit = omit, + source: Literal["yo", "en", "ha", "ig", "am", "pcm"], + target: Literal["yo", "en", "ha", "ig", "am", "pcm"], + text: str, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -100,8 +96,10 @@ def translate( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Translation: - """ - Translate + """Translate text from one language to another. + + Select the source and target + languages, and get text in new language. Args: extra_headers: Send extra headers @@ -118,9 +116,6 @@ def translate( { "source": source, "target": target, - "file_id": file_id, - "instructions": instructions, - "model": model, "text": text, }, text_translate_params.TextTranslateParams, @@ -155,7 +150,7 @@ def with_streaming_response(self) -> AsyncTextResourceWithStreamingResponse: async def tone_mark( self, *, - language: Literal["yo", "en", "ha", "ig", "am"], + language: Literal["yo", "en", "ha", "ig", "am", "pcm"], text: str, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -165,7 +160,7 @@ async def tone_mark( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Diacritics: """ - Tone Mark + Add appopriate tone marks to text. Args: extra_headers: Send extra headers @@ -194,12 +189,9 @@ async def tone_mark( async def translate( self, *, - source: Literal["yo", "en", "ha", "ig", "am"], - target: Literal["yo", "en", "ha", "ig", "am"], - file_id: Optional[str] | Omit = omit, - instructions: Optional[str] | Omit = omit, - model: Literal["human", "auto"] | Omit = omit, - text: Optional[str] | Omit = omit, + source: Literal["yo", "en", "ha", "ig", "am", "pcm"], + target: Literal["yo", "en", "ha", "ig", "am", "pcm"], + text: str, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -207,8 +199,10 @@ async def translate( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Translation: - """ - Translate + """Translate text from one language to another. + + Select the source and target + languages, and get text in new language. Args: extra_headers: Send extra headers @@ -225,9 +219,6 @@ async def translate( { "source": source, "target": target, - "file_id": file_id, - "instructions": instructions, - "model": model, "text": text, }, text_translate_params.TextTranslateParams, diff --git a/src/spitch/types/__init__.py b/src/spitch/types/__init__.py index a27cec6..b4de01c 100644 --- a/src/spitch/types/__init__.py +++ b/src/spitch/types/__init__.py @@ -2,20 +2,18 @@ from __future__ import annotations -from .job import Job as Job from .file import File as File -from .jobs import Jobs as Jobs from .files import Files as Files +from .segment import Segment as Segment from .diacritics import Diacritics as Diacritics from .file_usage import FileUsage as FileUsage from .translation import Translation as Translation -from .transcription import Transcription as Transcription -from .job_list_params import JobListParams as JobListParams from .file_list_params import FileListParams as FileListParams from .file_upload_params import FileUploadParams as FileUploadParams +from .file_delete_response import FileDeleteResponse as FileDeleteResponse from .file_download_params import FileDownloadParams as FileDownloadParams from .text_tone_mark_params import TextToneMarkParams as TextToneMarkParams from .text_translate_params import TextTranslateParams as TextTranslateParams -from .file_download_response import FileDownloadResponse as FileDownloadResponse from .speech_generate_params import SpeechGenerateParams as SpeechGenerateParams from .speech_transcribe_params import SpeechTranscribeParams as SpeechTranscribeParams +from .speech_transcribe_response import SpeechTranscribeResponse as SpeechTranscribeResponse diff --git a/src/spitch/types/diacritics.py b/src/spitch/types/diacritics.py index 0f3a044..e9b1e07 100644 --- a/src/spitch/types/diacritics.py +++ b/src/spitch/types/diacritics.py @@ -6,15 +6,6 @@ class Diacritics(BaseModel): - """ - Response model for text diacritization requests. - - Attributes: - request_id: Unique identifier for the diacritization request - text: Text with added diacritical marks for proper pronunciation - language: Language code for the diacritized text - """ - request_id: str text: str diff --git a/src/spitch/types/file.py b/src/spitch/types/file.py index 39e03e5..5961517 100644 --- a/src/spitch/types/file.py +++ b/src/spitch/types/file.py @@ -2,7 +2,6 @@ from typing import Optional from datetime import datetime -from typing_extensions import Literal from .._models import BaseModel @@ -10,15 +9,7 @@ class File(BaseModel): - """ - description of a file. - Attributes: - file_id: unique identifier for the file. - status: status of the file, `processing` or `ready` - original_name: original name of the file. If the file was uploaded via API - """ - - category: Optional[str] = None + """Metadata info for this file.""" content_type: Optional[str] = None @@ -30,6 +21,6 @@ class File(BaseModel): size_bytes: Optional[int] = None - status: Literal["uploading", "ready"] + status: str uploaded_by: Optional[str] = None diff --git a/src/spitch/types/file_delete_response.py b/src/spitch/types/file_delete_response.py new file mode 100644 index 0000000..dd819ca --- /dev/null +++ b/src/spitch/types/file_delete_response.py @@ -0,0 +1,11 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel + +__all__ = ["FileDeleteResponse"] + + +class FileDeleteResponse(BaseModel): + status: Optional[bool] = None diff --git a/src/spitch/types/file_download_response.py b/src/spitch/types/file_download_response.py deleted file mode 100644 index c9e58d0..0000000 --- a/src/spitch/types/file_download_response.py +++ /dev/null @@ -1,23 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from datetime import datetime - -from .._models import BaseModel - -__all__ = ["FileDownloadResponse"] - - -class FileDownloadResponse(BaseModel): - """Response model for file download URLs. - - Attributes: - file_id: Unique identifier for the file - url: Pre-signed download URL - expires_at: When the download URL expires - """ - - expires_at: datetime - - file_id: str - - url: str diff --git a/src/spitch/types/file_list_params.py b/src/spitch/types/file_list_params.py index 7625e59..1ce6011 100644 --- a/src/spitch/types/file_list_params.py +++ b/src/spitch/types/file_list_params.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Optional -from typing_extensions import Literal, TypedDict +from typing_extensions import TypedDict __all__ = ["FileListParams"] @@ -12,5 +12,3 @@ class FileListParams(TypedDict, total=False): cursor: Optional[str] limit: int - - status: Optional[Literal["uploading", "ready"]] diff --git a/src/spitch/types/file_usage.py b/src/spitch/types/file_usage.py index 93cb866..d866f2e 100644 --- a/src/spitch/types/file_usage.py +++ b/src/spitch/types/file_usage.py @@ -1,27 +1,24 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from typing import Optional + from .._models import BaseModel __all__ = ["FileUsage"] class FileUsage(BaseModel): - """Storage usage information for an organization. - - Attributes: - total: Human-readable total storage limit - used: Human-readable used storage amount - total_bytes: Total storage limit in bytes - used_bytes: Used storage amount in bytes - num_files: Number of files stored - """ - - num_files: int + num_files: Optional[int] = None + """number of files available.""" - total: str + total: Optional[str] = None + """total storage available in human-readable format""" - total_bytes: int + total_bytes: Optional[int] = None + """storage used in bytes""" - used: str + used: Optional[str] = None + """storage used in human-readable format""" - used_bytes: int + used_bytes: Optional[int] = None + """total storage available in bytes""" diff --git a/src/spitch/types/files.py b/src/spitch/types/files.py index 6328733..c29ba0d 100644 --- a/src/spitch/types/files.py +++ b/src/spitch/types/files.py @@ -9,12 +9,7 @@ class Files(BaseModel): - """Response model for listing files. - - Attributes: - items: List of file metadata responses - next_cursor: Optional cursor for pagination to get next page of results - """ + """an array of file information.""" items: List[File] diff --git a/src/spitch/types/job.py b/src/spitch/types/job.py deleted file mode 100644 index da92372..0000000 --- a/src/spitch/types/job.py +++ /dev/null @@ -1,21 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from datetime import datetime - -from .._models import BaseModel - -__all__ = ["Job"] - - -class Job(BaseModel): - """Metadata model for job metadata.""" - - created_by: str - - due_date: datetime - - job_id: str - - org_id: str - - status: str diff --git a/src/spitch/types/job_list_params.py b/src/spitch/types/job_list_params.py deleted file mode 100644 index 2ef3b95..0000000 --- a/src/spitch/types/job_list_params.py +++ /dev/null @@ -1,16 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Optional -from typing_extensions import Literal, TypedDict - -__all__ = ["JobListParams"] - - -class JobListParams(TypedDict, total=False): - cursor: Optional[str] - - limit: int - - status: Optional[Literal["queued", "in_progress", "finished"]] diff --git a/src/spitch/types/jobs.py b/src/spitch/types/jobs.py deleted file mode 100644 index 7df3100..0000000 --- a/src/spitch/types/jobs.py +++ /dev/null @@ -1,21 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List, Optional - -from .job import Job -from .._models import BaseModel - -__all__ = ["Jobs"] - - -class Jobs(BaseModel): - """Response model for listing jobs. - - Attributes: - items: list of job metadata responses - next_cursor: Optional cursor for pagination to get next page of results - """ - - items: List[Job] - - next_cursor: Optional[str] = None diff --git a/src/spitch/types/segment.py b/src/spitch/types/segment.py new file mode 100644 index 0000000..3243c3e --- /dev/null +++ b/src/spitch/types/segment.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel + +__all__ = ["Segment"] + + +class Segment(BaseModel): + """a segment (sentence or word-level) of the transcript. + + It contains a start and end time. + """ + + end: float + """the exact time when this segment ended.""" + + start: float + """the exact time when this segment started.""" + + text: str + """the text that belongs in this timeframe.""" diff --git a/src/spitch/types/speech_generate_params.py b/src/spitch/types/speech_generate_params.py index a3ac63e..375af2b 100644 --- a/src/spitch/types/speech_generate_params.py +++ b/src/spitch/types/speech_generate_params.py @@ -3,13 +3,15 @@ from __future__ import annotations from typing import Optional -from typing_extensions import Literal, Required, TypedDict +from typing_extensions import Literal, Required, Annotated, TypedDict + +from .._utils import PropertyInfo __all__ = ["SpeechGenerateParams"] class SpeechGenerateParams(TypedDict, total=False): - language: Required[Literal["yo", "en", "ha", "ig", "am"]] + language: Required[Literal["yo", "en", "ha", "ig", "am", "pcm"]] text: Required[str] @@ -40,6 +42,6 @@ class SpeechGenerateParams(TypedDict, total=False): ] ] - format: Literal["wav", "mp3", "ogg_opus", "webm_opus", "flac", "pcm_s16le", "mulaw", "alaw"] + model: Optional[str] - model: Optional[Literal["legacy"]] + spitch_x_data_retention: Annotated[bool, PropertyInfo(alias="Spitch-X-Data-Retention")] diff --git a/src/spitch/types/speech_transcribe_params.py b/src/spitch/types/speech_transcribe_params.py index 8909a68..6555c21 100644 --- a/src/spitch/types/speech_transcribe_params.py +++ b/src/spitch/types/speech_transcribe_params.py @@ -3,22 +3,25 @@ from __future__ import annotations from typing import Union, Optional -from typing_extensions import Literal, Required, TypedDict +from typing_extensions import Literal, Required, Annotated, TypedDict from .._types import FileTypes +from .._utils import PropertyInfo __all__ = ["SpeechTranscribeParams"] class SpeechTranscribeParams(TypedDict, total=False): - language: Required[Literal["yo", "en", "ha", "ig", "am"]] + language: Required[Literal["yo", "en", "ha", "ig", "am", "pcm"]] content: Union[FileTypes, str, None] - model: Optional[Literal["mansa_v1", "legacy", "human"]] + model: Optional[Literal["mansa_v1", "legacy"]] special_words: Optional[str] timestamp: Optional[Literal["sentence", "word", "none"]] url: Optional[str] + + spitch_x_data_retention: Annotated[bool, PropertyInfo(alias="Spitch-X-Data-Retention")] diff --git a/src/spitch/types/speech_transcribe_response.py b/src/spitch/types/speech_transcribe_response.py new file mode 100644 index 0000000..96e3e4a --- /dev/null +++ b/src/spitch/types/speech_transcribe_response.py @@ -0,0 +1,23 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .segment import Segment +from .._models import BaseModel + +__all__ = ["SpeechTranscribeResponse"] + + +class SpeechTranscribeResponse(BaseModel): + """Response from speech-to-text.""" + + request_id: str + """for audit purposes.""" + + text: str + + segments: Optional[List[Segment]] = None + """sentence-level or word-level groupings of your transcript. + + Each sentence (or word) will fall within a time range. + """ diff --git a/src/spitch/types/text_tone_mark_params.py b/src/spitch/types/text_tone_mark_params.py index c79ca88..228c8f0 100644 --- a/src/spitch/types/text_tone_mark_params.py +++ b/src/spitch/types/text_tone_mark_params.py @@ -8,6 +8,6 @@ class TextToneMarkParams(TypedDict, total=False): - language: Required[Literal["yo", "en", "ha", "ig", "am"]] + language: Required[Literal["yo", "en", "ha", "ig", "am", "pcm"]] text: Required[str] diff --git a/src/spitch/types/text_translate_params.py b/src/spitch/types/text_translate_params.py index 073c81d..05fce7a 100644 --- a/src/spitch/types/text_translate_params.py +++ b/src/spitch/types/text_translate_params.py @@ -2,21 +2,14 @@ from __future__ import annotations -from typing import Optional from typing_extensions import Literal, Required, TypedDict __all__ = ["TextTranslateParams"] class TextTranslateParams(TypedDict, total=False): - source: Required[Literal["yo", "en", "ha", "ig", "am"]] + source: Required[Literal["yo", "en", "ha", "ig", "am", "pcm"]] - target: Required[Literal["yo", "en", "ha", "ig", "am"]] + target: Required[Literal["yo", "en", "ha", "ig", "am", "pcm"]] - file_id: Optional[str] - - instructions: Optional[str] - - model: Literal["human", "auto"] - - text: Optional[str] + text: Required[str] diff --git a/src/spitch/types/transcription.py b/src/spitch/types/transcription.py deleted file mode 100644 index 36ecc1f..0000000 --- a/src/spitch/types/transcription.py +++ /dev/null @@ -1,23 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List, Optional - -from .._models import BaseModel - -__all__ = ["Transcription", "Timestamp"] - - -class Timestamp(BaseModel): - end: float - - start: float - - text: str - - -class Transcription(BaseModel): - request_id: str - - text: str - - timestamps: Optional[List[Timestamp]] = None diff --git a/src/spitch/types/translation.py b/src/spitch/types/translation.py index f5588ac..8d0ef60 100644 --- a/src/spitch/types/translation.py +++ b/src/spitch/types/translation.py @@ -1,24 +1,11 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional -from datetime import datetime - from .._models import BaseModel __all__ = ["Translation"] class Translation(BaseModel): - """Translation result model. - - Attributes: - request_id (UUID): Unique ID for this request. - text: translated text. - due_date: used when model is `human`. the date you can expect the translation to be delivered - """ - request_id: str text: str - - due_date: Optional[datetime] = None diff --git a/tests/api_resources/test_files.py b/tests/api_resources/test_files.py index b3dbbd9..7f37b8b 100644 --- a/tests/api_resources/test_files.py +++ b/tests/api_resources/test_files.py @@ -9,11 +9,7 @@ from spitch import Spitch, AsyncSpitch from tests.utils import assert_matches_type -from spitch.types import ( - File, - FileUsage, - FileDownloadResponse, -) +from spitch.types import File, FileUsage, FileDeleteResponse from spitch.pagination import SyncFilesCursor, AsyncFilesCursor base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -32,7 +28,6 @@ def test_method_list_with_all_params(self, client: Spitch) -> None: file = client.files.list( cursor="cursor", limit=99, - status="uploading", ) assert_matches_type(SyncFilesCursor[File], file, path=["response"]) @@ -61,7 +56,7 @@ def test_method_delete(self, client: Spitch) -> None: file = client.files.delete( "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) - assert_matches_type(object, file, path=["response"]) + assert_matches_type(FileDeleteResponse, file, path=["response"]) @parametrize def test_raw_response_delete(self, client: Spitch) -> None: @@ -72,7 +67,7 @@ def test_raw_response_delete(self, client: Spitch) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" file = response.parse() - assert_matches_type(object, file, path=["response"]) + assert_matches_type(FileDeleteResponse, file, path=["response"]) @parametrize def test_streaming_response_delete(self, client: Spitch) -> None: @@ -83,7 +78,7 @@ def test_streaming_response_delete(self, client: Spitch) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" file = response.parse() - assert_matches_type(object, file, path=["response"]) + assert_matches_type(FileDeleteResponse, file, path=["response"]) assert cast(Any, response.is_closed) is True @@ -99,7 +94,7 @@ def test_method_download(self, client: Spitch) -> None: file = client.files.download( file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) - assert_matches_type(FileDownloadResponse, file, path=["response"]) + assert_matches_type(object, file, path=["response"]) @parametrize def test_method_download_with_all_params(self, client: Spitch) -> None: @@ -107,7 +102,7 @@ def test_method_download_with_all_params(self, client: Spitch) -> None: file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ttl=60, ) - assert_matches_type(FileDownloadResponse, file, path=["response"]) + assert_matches_type(object, file, path=["response"]) @parametrize def test_raw_response_download(self, client: Spitch) -> None: @@ -118,7 +113,7 @@ def test_raw_response_download(self, client: Spitch) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" file = response.parse() - assert_matches_type(FileDownloadResponse, file, path=["response"]) + assert_matches_type(object, file, path=["response"]) @parametrize def test_streaming_response_download(self, client: Spitch) -> None: @@ -129,7 +124,7 @@ def test_streaming_response_download(self, client: Spitch) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" file = response.parse() - assert_matches_type(FileDownloadResponse, file, path=["response"]) + assert_matches_type(object, file, path=["response"]) assert cast(Any, response.is_closed) is True @@ -250,7 +245,6 @@ async def test_method_list_with_all_params(self, async_client: AsyncSpitch) -> N file = await async_client.files.list( cursor="cursor", limit=99, - status="uploading", ) assert_matches_type(AsyncFilesCursor[File], file, path=["response"]) @@ -279,7 +273,7 @@ async def test_method_delete(self, async_client: AsyncSpitch) -> None: file = await async_client.files.delete( "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) - assert_matches_type(object, file, path=["response"]) + assert_matches_type(FileDeleteResponse, file, path=["response"]) @parametrize async def test_raw_response_delete(self, async_client: AsyncSpitch) -> None: @@ -290,7 +284,7 @@ async def test_raw_response_delete(self, async_client: AsyncSpitch) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" file = await response.parse() - assert_matches_type(object, file, path=["response"]) + assert_matches_type(FileDeleteResponse, file, path=["response"]) @parametrize async def test_streaming_response_delete(self, async_client: AsyncSpitch) -> None: @@ -301,7 +295,7 @@ async def test_streaming_response_delete(self, async_client: AsyncSpitch) -> Non assert response.http_request.headers.get("X-Stainless-Lang") == "python" file = await response.parse() - assert_matches_type(object, file, path=["response"]) + assert_matches_type(FileDeleteResponse, file, path=["response"]) assert cast(Any, response.is_closed) is True @@ -317,7 +311,7 @@ async def test_method_download(self, async_client: AsyncSpitch) -> None: file = await async_client.files.download( file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ) - assert_matches_type(FileDownloadResponse, file, path=["response"]) + assert_matches_type(object, file, path=["response"]) @parametrize async def test_method_download_with_all_params(self, async_client: AsyncSpitch) -> None: @@ -325,7 +319,7 @@ async def test_method_download_with_all_params(self, async_client: AsyncSpitch) file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", ttl=60, ) - assert_matches_type(FileDownloadResponse, file, path=["response"]) + assert_matches_type(object, file, path=["response"]) @parametrize async def test_raw_response_download(self, async_client: AsyncSpitch) -> None: @@ -336,7 +330,7 @@ async def test_raw_response_download(self, async_client: AsyncSpitch) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" file = await response.parse() - assert_matches_type(FileDownloadResponse, file, path=["response"]) + assert_matches_type(object, file, path=["response"]) @parametrize async def test_streaming_response_download(self, async_client: AsyncSpitch) -> None: @@ -347,7 +341,7 @@ async def test_streaming_response_download(self, async_client: AsyncSpitch) -> N assert response.http_request.headers.get("X-Stainless-Lang") == "python" file = await response.parse() - assert_matches_type(FileDownloadResponse, file, path=["response"]) + assert_matches_type(object, file, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_jobs.py b/tests/api_resources/test_jobs.py deleted file mode 100644 index 829ebee..0000000 --- a/tests/api_resources/test_jobs.py +++ /dev/null @@ -1,169 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import os -from typing import Any, cast - -import pytest - -from spitch import Spitch, AsyncSpitch -from tests.utils import assert_matches_type -from spitch.types import Job -from spitch.pagination import SyncFilesCursor, AsyncFilesCursor - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestJobs: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @parametrize - def test_method_list(self, client: Spitch) -> None: - job = client.jobs.list() - assert_matches_type(SyncFilesCursor[Job], job, path=["response"]) - - @parametrize - def test_method_list_with_all_params(self, client: Spitch) -> None: - job = client.jobs.list( - cursor="cursor", - limit=99, - status="queued", - ) - assert_matches_type(SyncFilesCursor[Job], job, path=["response"]) - - @parametrize - def test_raw_response_list(self, client: Spitch) -> None: - response = client.jobs.with_raw_response.list() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - job = response.parse() - assert_matches_type(SyncFilesCursor[Job], job, path=["response"]) - - @parametrize - def test_streaming_response_list(self, client: Spitch) -> None: - with client.jobs.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - job = response.parse() - assert_matches_type(SyncFilesCursor[Job], job, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @parametrize - def test_method_get(self, client: Spitch) -> None: - job = client.jobs.get( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(Job, job, path=["response"]) - - @parametrize - def test_raw_response_get(self, client: Spitch) -> None: - response = client.jobs.with_raw_response.get( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - job = response.parse() - assert_matches_type(Job, job, path=["response"]) - - @parametrize - def test_streaming_response_get(self, client: Spitch) -> None: - with client.jobs.with_streaming_response.get( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - job = response.parse() - assert_matches_type(Job, job, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @parametrize - def test_path_params_get(self, client: Spitch) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `job_id` but received ''"): - client.jobs.with_raw_response.get( - "", - ) - - -class TestAsyncJobs: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) - - @parametrize - async def test_method_list(self, async_client: AsyncSpitch) -> None: - job = await async_client.jobs.list() - assert_matches_type(AsyncFilesCursor[Job], job, path=["response"]) - - @parametrize - async def test_method_list_with_all_params(self, async_client: AsyncSpitch) -> None: - job = await async_client.jobs.list( - cursor="cursor", - limit=99, - status="queued", - ) - assert_matches_type(AsyncFilesCursor[Job], job, path=["response"]) - - @parametrize - async def test_raw_response_list(self, async_client: AsyncSpitch) -> None: - response = await async_client.jobs.with_raw_response.list() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - job = await response.parse() - assert_matches_type(AsyncFilesCursor[Job], job, path=["response"]) - - @parametrize - async def test_streaming_response_list(self, async_client: AsyncSpitch) -> None: - async with async_client.jobs.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - job = await response.parse() - assert_matches_type(AsyncFilesCursor[Job], job, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @parametrize - async def test_method_get(self, async_client: AsyncSpitch) -> None: - job = await async_client.jobs.get( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - assert_matches_type(Job, job, path=["response"]) - - @parametrize - async def test_raw_response_get(self, async_client: AsyncSpitch) -> None: - response = await async_client.jobs.with_raw_response.get( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - job = await response.parse() - assert_matches_type(Job, job, path=["response"]) - - @parametrize - async def test_streaming_response_get(self, async_client: AsyncSpitch) -> None: - async with async_client.jobs.with_streaming_response.get( - "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - job = await response.parse() - assert_matches_type(Job, job, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @parametrize - async def test_path_params_get(self, async_client: AsyncSpitch) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `job_id` but received ''"): - await async_client.jobs.with_raw_response.get( - "", - ) diff --git a/tests/api_resources/test_speech.py b/tests/api_resources/test_speech.py index 62290bd..1dcb99f 100644 --- a/tests/api_resources/test_speech.py +++ b/tests/api_resources/test_speech.py @@ -11,7 +11,7 @@ from spitch import Spitch, AsyncSpitch from tests.utils import assert_matches_type -from spitch.types import Transcription +from spitch.types import SpeechTranscribeResponse from spitch._response import ( BinaryAPIResponse, AsyncBinaryAPIResponse, @@ -47,8 +47,8 @@ def test_method_generate_with_all_params(self, client: Spitch, respx_mock: MockR language="yo", text="text", voice="sade", - format="wav", - model="legacy", + model="model", + spitch_x_data_retention=True, ) assert speech.is_closed assert speech.json() == {"foo": "bar"} @@ -94,7 +94,7 @@ def test_method_transcribe(self, client: Spitch) -> None: speech = client.speech.transcribe( language="yo", ) - assert_matches_type(Transcription, speech, path=["response"]) + assert_matches_type(SpeechTranscribeResponse, speech, path=["response"]) @parametrize def test_method_transcribe_with_all_params(self, client: Spitch) -> None: @@ -105,8 +105,9 @@ def test_method_transcribe_with_all_params(self, client: Spitch) -> None: special_words="special_words", timestamp="sentence", url="url", + spitch_x_data_retention=True, ) - assert_matches_type(Transcription, speech, path=["response"]) + assert_matches_type(SpeechTranscribeResponse, speech, path=["response"]) @parametrize def test_raw_response_transcribe(self, client: Spitch) -> None: @@ -117,7 +118,7 @@ def test_raw_response_transcribe(self, client: Spitch) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" speech = response.parse() - assert_matches_type(Transcription, speech, path=["response"]) + assert_matches_type(SpeechTranscribeResponse, speech, path=["response"]) @parametrize def test_streaming_response_transcribe(self, client: Spitch) -> None: @@ -128,7 +129,7 @@ def test_streaming_response_transcribe(self, client: Spitch) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" speech = response.parse() - assert_matches_type(Transcription, speech, path=["response"]) + assert_matches_type(SpeechTranscribeResponse, speech, path=["response"]) assert cast(Any, response.is_closed) is True @@ -160,8 +161,8 @@ async def test_method_generate_with_all_params(self, async_client: AsyncSpitch, language="yo", text="text", voice="sade", - format="wav", - model="legacy", + model="model", + spitch_x_data_retention=True, ) assert speech.is_closed assert await speech.json() == {"foo": "bar"} @@ -207,7 +208,7 @@ async def test_method_transcribe(self, async_client: AsyncSpitch) -> None: speech = await async_client.speech.transcribe( language="yo", ) - assert_matches_type(Transcription, speech, path=["response"]) + assert_matches_type(SpeechTranscribeResponse, speech, path=["response"]) @parametrize async def test_method_transcribe_with_all_params(self, async_client: AsyncSpitch) -> None: @@ -218,8 +219,9 @@ async def test_method_transcribe_with_all_params(self, async_client: AsyncSpitch special_words="special_words", timestamp="sentence", url="url", + spitch_x_data_retention=True, ) - assert_matches_type(Transcription, speech, path=["response"]) + assert_matches_type(SpeechTranscribeResponse, speech, path=["response"]) @parametrize async def test_raw_response_transcribe(self, async_client: AsyncSpitch) -> None: @@ -230,7 +232,7 @@ async def test_raw_response_transcribe(self, async_client: AsyncSpitch) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" speech = await response.parse() - assert_matches_type(Transcription, speech, path=["response"]) + assert_matches_type(SpeechTranscribeResponse, speech, path=["response"]) @parametrize async def test_streaming_response_transcribe(self, async_client: AsyncSpitch) -> None: @@ -241,6 +243,6 @@ async def test_streaming_response_transcribe(self, async_client: AsyncSpitch) -> assert response.http_request.headers.get("X-Stainless-Lang") == "python" speech = await response.parse() - assert_matches_type(Transcription, speech, path=["response"]) + assert_matches_type(SpeechTranscribeResponse, speech, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_text.py b/tests/api_resources/test_text.py index cd78689..9630198 100644 --- a/tests/api_resources/test_text.py +++ b/tests/api_resources/test_text.py @@ -56,17 +56,6 @@ def test_method_translate(self, client: Spitch) -> None: text = client.text.translate( source="yo", target="yo", - ) - assert_matches_type(Translation, text, path=["response"]) - - @parametrize - def test_method_translate_with_all_params(self, client: Spitch) -> None: - text = client.text.translate( - source="yo", - target="yo", - file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - instructions="instructions", - model="human", text="text", ) assert_matches_type(Translation, text, path=["response"]) @@ -76,6 +65,7 @@ def test_raw_response_translate(self, client: Spitch) -> None: response = client.text.with_raw_response.translate( source="yo", target="yo", + text="text", ) assert response.is_closed is True @@ -88,6 +78,7 @@ def test_streaming_response_translate(self, client: Spitch) -> None: with client.text.with_streaming_response.translate( source="yo", target="yo", + text="text", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -142,17 +133,6 @@ async def test_method_translate(self, async_client: AsyncSpitch) -> None: text = await async_client.text.translate( source="yo", target="yo", - ) - assert_matches_type(Translation, text, path=["response"]) - - @parametrize - async def test_method_translate_with_all_params(self, async_client: AsyncSpitch) -> None: - text = await async_client.text.translate( - source="yo", - target="yo", - file_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - instructions="instructions", - model="human", text="text", ) assert_matches_type(Translation, text, path=["response"]) @@ -162,6 +142,7 @@ async def test_raw_response_translate(self, async_client: AsyncSpitch) -> None: response = await async_client.text.with_raw_response.translate( source="yo", target="yo", + text="text", ) assert response.is_closed is True @@ -174,6 +155,7 @@ async def test_streaming_response_translate(self, async_client: AsyncSpitch) -> async with async_client.text.with_streaming_response.translate( source="yo", target="yo", + text="text", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" From 0da0a3e5ed565d2d0c62526c466f6a3db25f798b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 17:05:34 +0000 Subject: [PATCH 17/17] release: 1.42.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ pyproject.toml | 2 +- src/spitch/_version.py | 2 +- 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index b34aa0b..507912c 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.41.2" + ".": "1.42.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 29b1836..f34ecde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,35 @@ # Changelog +## 1.42.0 (2026-01-21) + +Full Changelog: [v1.41.2...v1.42.0](https://github.com/spi-tch/spitch-python/compare/v1.41.2...v1.42.0) + +### Features + +* **api:** manual updates ([12f75ef](https://github.com/spi-tch/spitch-python/commit/12f75eff7f034196f0f453dfb4b4c800234d9198)) +* **client:** add support for binary request streaming ([0c96451](https://github.com/spi-tch/spitch-python/commit/0c96451d56e5897221e1a5e628c557f8d47df911)) +* **files:** add support for string alternative to file upload type ([7c71520](https://github.com/spi-tch/spitch-python/commit/7c7152096acff1ab2777e88ea6edf8e74762a366)) + + +### Bug Fixes + +* ensure streams are always closed ([2eaa7db](https://github.com/spi-tch/spitch-python/commit/2eaa7dbb883d8ae3ecf90da45dce26252a996f62)) +* **types:** allow pyright to infer TypedDict types within SequenceNotStr ([49b3944](https://github.com/spi-tch/spitch-python/commit/49b39447feb57724b18dbabd08da07ff6fe10164)) +* use async_to_httpx_files in patch method ([d6681dc](https://github.com/spi-tch/spitch-python/commit/d6681dc12cb59df8843b6adbe09095efde66548b)) + + +### Chores + +* add missing docstrings ([5706a11](https://github.com/spi-tch/spitch-python/commit/5706a112946f0c294388455c32cf5c1e1bad270a)) +* **deps:** mypy 1.18.1 has a regression, pin to 1.17 ([ebbc73f](https://github.com/spi-tch/spitch-python/commit/ebbc73f41c762dc2adcbb3013ea695fdb3c50e2d)) +* **docs:** use environment variables for authentication in code snippets ([e74350e](https://github.com/spi-tch/spitch-python/commit/e74350e037dc2136577aa758e493e0261ce70166)) +* **internal:** add `--fix` argument to lint script ([697bfd0](https://github.com/spi-tch/spitch-python/commit/697bfd03e8bc2c855e36fbfcc23f6ecf7d740652)) +* **internal:** add missing files argument to base client ([d52ec35](https://github.com/spi-tch/spitch-python/commit/d52ec358ed429f302ff0c035d1b8b94cbe24e3e0)) +* **internal:** codegen related update ([d11c298](https://github.com/spi-tch/spitch-python/commit/d11c2988b89fde8ef890fe072869caf1692fe56a)) +* **internal:** update `actions/checkout` version ([9dfcb50](https://github.com/spi-tch/spitch-python/commit/9dfcb509c7bed5382e2de4663838fc0583774bc2)) +* speedup initial import ([8b385df](https://github.com/spi-tch/spitch-python/commit/8b385dffc6d34d7cbda6ba197d08637c98a6af37)) +* update lockfile ([442785c](https://github.com/spi-tch/spitch-python/commit/442785c97d9cb3ccee5403666a4cdbbf00338cea)) + ## 1.41.2 (2025-11-25) Full Changelog: [v1.41.1...v1.41.2](https://github.com/spi-tch/spitch-python/compare/v1.41.1...v1.41.2) diff --git a/pyproject.toml b/pyproject.toml index 392d40e..fac1536 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "spitch" -version = "1.41.2" +version = "1.42.0" description = "The official Python library for the spitch API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/spitch/_version.py b/src/spitch/_version.py index 0e8814f..0c0bb79 100644 --- a/src/spitch/_version.py +++ b/src/spitch/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "spitch" -__version__ = "1.41.2" # x-release-please-version +__version__ = "1.42.0" # x-release-please-version