From 1340eb172500c78d9ce73bfe8ce557bcdb40c4d7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 29 May 2026 10:42:46 +0000 Subject: [PATCH 1/8] feat: rename /docs/auth to /docs/overview --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index cc6bfd14..73de080f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 14 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/turbopuffer-benesch/turbopuffer-6717a82f2b22fd2b55bce7acb7d5e9a694c51132a5522626d39684bff6e0cb83.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/turbopuffer-benesch/turbopuffer-004362507d55934860a5baed6f50f0dc17496c92b5782ce3b45b44bd0c317de3.yml openapi_spec_hash: 647e6105fbcaba7eaed15f1859f1c463 -config_hash: 3917af929e17def75be204eb7e4000fa +config_hash: ba6a87e0ff4b331b7c97f041417e0333 From 204b46bf15c5694bdd38339b8471c46e16e697a0 Mon Sep 17 00:00:00 2001 From: Jan Teske Date: Fri, 29 May 2026 21:33:34 +0200 Subject: [PATCH 2/8] chore: fix API docs links (#235) --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2db55f3a..e5637be4 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ PyPI version -The turbopuffer Python library provides convenient access to the Turbopuffer HTTP API from any Python 3.9+ +The turbopuffer Python library provides convenient access to the [turbopuffer HTTP API](https://turbopuffer.com/docs/overview) from any Python 3.9+ application. The library includes type definitions for all request params and response fields, and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx). @@ -11,7 +11,7 @@ It is generated with [Stainless](https://www.stainless.com/). ## MCP Server -Use the Turbopuffer MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application. +Use the turbopuffer MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application. [![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=%40turbopuffer%2Fturbopuffer-mcp&config=eyJuYW1lIjoiQHR1cmJvcHVmZmVyL3R1cmJvcHVmZmVyLW1jcCIsInRyYW5zcG9ydCI6Imh0dHAiLCJ1cmwiOiJodHRwczovL3R1cmJvcHVmZmVyLnN0bG1jcC5jb20iLCJoZWFkZXJzIjp7IngtdHVyYm9wdWZmZXItYXBpLWtleSI6InRwdWZfQTEuLi4ifX0) [![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22%40turbopuffer%2Fturbopuffer-mcp%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fturbopuffer.stlmcp.com%22%2C%22headers%22%3A%7B%22x-turbopuffer-api-key%22%3A%22tpuf_A1...%22%7D%7D) @@ -20,7 +20,7 @@ Use the Turbopuffer MCP Server to enable AI assistants to interact with this API ## Documentation -The HTTP API documentation can be found at [turbopuffer.com/docs](https://turbopuffer.com/docs). +The HTTP API documentation can be found at [turbopuffer.com/docs/overview](https://turbopuffer.com/docs/overview). ## Installation @@ -172,7 +172,7 @@ Typed requests and responses provide autocomplete and documentation within your ## Pagination -List methods in the Turbopuffer API are paginated. +List methods in the turbopuffer API are paginated. This library provides auto-paginating iterators with each list response, so you do not have to request successive pages manually: From ca71729755294ed2eb073453e33baa8c89548063 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 29 May 2026 20:56:10 +0000 Subject: [PATCH 3/8] feat: spec: add SDK support for native embedding --- .stats.yml | 6 ++-- api.md | 3 ++ src/turbopuffer/types/__init__.py | 5 +++ src/turbopuffer/types/attribute_embed.py | 10 ++++++ .../types/attribute_embed_config.py | 32 +++++++++++++++++++ .../types/attribute_embed_config_param.py | 32 +++++++++++++++++++ .../types/attribute_embed_param.py | 12 +++++++ .../types/attribute_schema_config.py | 8 +++++ .../types/attribute_schema_config_param.py | 10 +++++- src/turbopuffer/types/custom.py | 7 +++- src/turbopuffer/types/embed_params.py | 17 ++++++++++ 11 files changed, 137 insertions(+), 5 deletions(-) create mode 100644 src/turbopuffer/types/attribute_embed.py create mode 100644 src/turbopuffer/types/attribute_embed_config.py create mode 100644 src/turbopuffer/types/attribute_embed_config_param.py create mode 100644 src/turbopuffer/types/attribute_embed_param.py create mode 100644 src/turbopuffer/types/embed_params.py diff --git a/.stats.yml b/.stats.yml index 73de080f..0600dd6d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 14 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/turbopuffer-benesch/turbopuffer-004362507d55934860a5baed6f50f0dc17496c92b5782ce3b45b44bd0c317de3.yml -openapi_spec_hash: 647e6105fbcaba7eaed15f1859f1c463 -config_hash: ba6a87e0ff4b331b7c97f041417e0333 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/turbopuffer-benesch/turbopuffer-a7a6fdfec853694b22f9773420ab0309a9271c7a44fb268cfdefb28cd1882c0f.yml +openapi_spec_hash: 302772c38d7fc6fb8127d302174d1f17 +config_hash: 8ea213ebc62d78cc8a79b52611c70900 diff --git a/api.md b/api.md index cdce2855..90a764b1 100644 --- a/api.md +++ b/api.md @@ -18,6 +18,8 @@ Types: from turbopuffer.types import ( AggregateBy, AggregationGroup, + AttributeEmbed, + AttributeEmbedConfig, AttributeSchema, AttributeSchemaConfig, AttributeType, @@ -29,6 +31,7 @@ from turbopuffer.types import ( CopyFromNamespaceParams, DecayParams, DistanceMetric, + EmbedParams, Encryption, FullTextSearch, FullTextSearchConfig, diff --git a/src/turbopuffer/types/__init__.py b/src/turbopuffer/types/__init__.py index 421e358c..fbcc49ed 100644 --- a/src/turbopuffer/types/__init__.py +++ b/src/turbopuffer/types/__init__.py @@ -13,6 +13,7 @@ from .encryption import Encryption as Encryption from .limit_param import LimitParam as LimitParam from .decay_params import DecayParams as DecayParams +from .embed_params import EmbedParams as EmbedParams from .fuzzy_params import FuzzyParams as FuzzyParams from .vector_param import VectorParam as VectorParam from .columns_param import ColumnsParam as ColumnsParam @@ -20,6 +21,7 @@ from .write_billing import WriteBilling as WriteBilling from .attribute_type import AttributeType as AttributeType from .pinning_config import PinningConfig as PinningConfig +from .attribute_embed import AttributeEmbed as AttributeEmbed from .distance_metric import DistanceMetric as DistanceMetric from .saturate_params import SaturateParams as SaturateParams from .vector_encoding import VectorEncoding as VectorEncoding @@ -32,6 +34,8 @@ from .bm25_clause_params import Bm25ClauseParams as Bm25ClauseParams from .namespace_metadata import NamespaceMetadata as NamespaceMetadata from .pinning_config_param import PinningConfigParam as PinningConfigParam +from .attribute_embed_param import AttributeEmbedParam as AttributeEmbedParam +from .attribute_embed_config import AttributeEmbedConfig as AttributeEmbedConfig from .attribute_schema_param import AttributeSchemaParam as AttributeSchemaParam from .full_text_search_param import FullTextSearchParam as FullTextSearchParam from .namespace_query_params import NamespaceQueryParams as NamespaceQueryParams @@ -48,6 +52,7 @@ from .namespace_schema_response import NamespaceSchemaResponse as NamespaceSchemaResponse from .copy_from_namespace_params import CopyFromNamespaceParams as CopyFromNamespaceParams from .namespace_copy_from_params import NamespaceCopyFromParams as NamespaceCopyFromParams +from .attribute_embed_config_param import AttributeEmbedConfigParam as AttributeEmbedConfigParam from .branch_from_namespace_params import BranchFromNamespaceParams as BranchFromNamespaceParams from .namespace_branch_from_params import NamespaceBranchFromParams as NamespaceBranchFromParams from .namespace_copy_from_response import NamespaceCopyFromResponse as NamespaceCopyFromResponse diff --git a/src/turbopuffer/types/attribute_embed.py b/src/turbopuffer/types/attribute_embed.py new file mode 100644 index 00000000..ca8e8b3d --- /dev/null +++ b/src/turbopuffer/types/attribute_embed.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Union +from typing_extensions import TypeAlias + +from .attribute_embed_config import AttributeEmbedConfig + +__all__ = ["AttributeEmbed"] + +AttributeEmbed: TypeAlias = Union[str, AttributeEmbedConfig, None] diff --git a/src/turbopuffer/types/attribute_embed_config.py b/src/turbopuffer/types/attribute_embed_config.py new file mode 100644 index 00000000..d6b8338b --- /dev/null +++ b/src/turbopuffer/types/attribute_embed_config.py @@ -0,0 +1,32 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel + +__all__ = ["AttributeEmbedConfig"] + + +class AttributeEmbedConfig(BaseModel): + """Configuration options for automatic embedding.""" + + model: str + """The model to use for embedding. + + See our documentation for a list of models supported in each region. + """ + + attribute: Optional[str] = None + """The name of an existing vector attribute to store embeddings in. + + If omitted, turbopuffer will generate a computed vector attribute named + `$embed_`. + """ + + dims: Optional[int] = None + """The dimensionality to embed at. + + If not set, will pick the default for this model. If you're storing embeddings + in an existing attribute, this can be omitted, and may not be set to a value + other than the dimensions of that attribute. + """ diff --git a/src/turbopuffer/types/attribute_embed_config_param.py b/src/turbopuffer/types/attribute_embed_config_param.py new file mode 100644 index 00000000..1ff2ce59 --- /dev/null +++ b/src/turbopuffer/types/attribute_embed_config_param.py @@ -0,0 +1,32 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["AttributeEmbedConfigParam"] + + +class AttributeEmbedConfigParam(TypedDict, total=False): + """Configuration options for automatic embedding.""" + + model: Required[str] + """The model to use for embedding. + + See our documentation for a list of models supported in each region. + """ + + attribute: str + """The name of an existing vector attribute to store embeddings in. + + If omitted, turbopuffer will generate a computed vector attribute named + `$embed_`. + """ + + dims: int + """The dimensionality to embed at. + + If not set, will pick the default for this model. If you're storing embeddings + in an existing attribute, this can be omitted, and may not be set to a value + other than the dimensions of that attribute. + """ diff --git a/src/turbopuffer/types/attribute_embed_param.py b/src/turbopuffer/types/attribute_embed_param.py new file mode 100644 index 00000000..fc65d093 --- /dev/null +++ b/src/turbopuffer/types/attribute_embed_param.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union +from typing_extensions import TypeAlias + +from .attribute_embed_config_param import AttributeEmbedConfigParam + +__all__ = ["AttributeEmbedParam"] + +AttributeEmbedParam: TypeAlias = Union[str, AttributeEmbedConfigParam] diff --git a/src/turbopuffer/types/attribute_schema_config.py b/src/turbopuffer/types/attribute_schema_config.py index 08caff3f..10f437c1 100644 --- a/src/turbopuffer/types/attribute_schema_config.py +++ b/src/turbopuffer/types/attribute_schema_config.py @@ -5,6 +5,7 @@ from .._models import BaseModel from .attribute_type import AttributeType +from .attribute_embed import AttributeEmbed from .distance_metric import DistanceMetric from .full_text_search import FullTextSearch from .sparse_distance_metric import SparseDistanceMetric @@ -48,6 +49,13 @@ class AttributeSchemaConfig(BaseModel): Can be a boolean or a detailed configuration object. """ + embed: Optional[AttributeEmbed] = None + """Whether to automatically embed this string attribute into a vector attribute. + + Can be a model name, a detailed configuration object, or `null` to remove an + existing embedding configuration. + """ + filterable: Optional[bool] = None """Whether or not the attributes can be used in filters.""" diff --git a/src/turbopuffer/types/attribute_schema_config_param.py b/src/turbopuffer/types/attribute_schema_config_param.py index d321003f..ab1d06d5 100644 --- a/src/turbopuffer/types/attribute_schema_config_param.py +++ b/src/turbopuffer/types/attribute_schema_config_param.py @@ -2,11 +2,12 @@ from __future__ import annotations -from typing import Union +from typing import Union, Optional from typing_extensions import Required, TypeAlias, TypedDict from .attribute_type import AttributeType from .distance_metric import DistanceMetric +from .attribute_embed_param import AttributeEmbedParam from .full_text_search_param import FullTextSearchParam from .sparse_distance_metric import SparseDistanceMetric @@ -49,6 +50,13 @@ class AttributeSchemaConfigParam(TypedDict, total=False): Can be a boolean or a detailed configuration object. """ + embed: Optional[AttributeEmbedParam] + """Whether to automatically embed this string attribute into a vector attribute. + + Can be a model name, a detailed configuration object, or `null` to remove an + existing embedding configuration. + """ + filterable: bool """Whether or not the attributes can be used in filters.""" diff --git a/src/turbopuffer/types/custom.py b/src/turbopuffer/types/custom.py index bd19564f..a61ebedf 100644 --- a/src/turbopuffer/types/custom.py +++ b/src/turbopuffer/types/custom.py @@ -3,6 +3,7 @@ from typing import Any, Tuple, Union, Literal, Mapping, Sequence, TypedDict from .decay_params import DecayParams +from .embed_params import EmbedParams from .fuzzy_params import FuzzyParams from .saturate_params import SaturateParams from .bm25_clause_params import Bm25ClauseParams @@ -11,7 +12,7 @@ AggregateBy = Union[Tuple[Literal["Count"]], Tuple[Literal["Sum"], str], Tuple[Literal["Count"], str]] ExprRefNew = TypedDict("ExprRefNew", {"$ref_new": str}) -Expr = ExprRefNew +Expr = Union[ExprRefNew, Tuple[Literal["Embed"], str], Tuple[Literal["Embed"], str, EmbedParams]] Filter = Union[ Tuple[str, Literal["Eq"], Any], Tuple[str, Literal["NotEq"], Any], @@ -52,7 +53,9 @@ GroupByFunction = Tuple[Literal["ForEachUnique"], str] GroupBy = Union[str, Mapping[str, GroupByFunction]] RankByAnn = Tuple[str, Literal["ANN"], Sequence[float]] +RankByAnnExpr = Tuple[str, Literal["ANN"], Expr] RankByKnn = Tuple[str, Literal["kNN"], Sequence[float]] +RankByKnnExpr = Tuple[str, Literal["kNN"], Expr] RankBySparseKnn = Tuple[str, Literal["SparseKNN"], Mapping[str, float]] RankByText = Union[ Tuple[str, Literal["BM25"], str], @@ -74,7 +77,9 @@ RankByAttributes = Sequence[RankByAttribute] RankBy = Union[ RankByAnn, + RankByAnnExpr, RankByKnn, + RankByKnnExpr, RankBySparseKnn, RankByText, RankByAttribute, diff --git a/src/turbopuffer/types/embed_params.py b/src/turbopuffer/types/embed_params.py new file mode 100644 index 00000000..d928a278 --- /dev/null +++ b/src/turbopuffer/types/embed_params.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["EmbedParams"] + + +class EmbedParams(TypedDict, total=False): + """Additional (optional) parameters for the Embed expression.""" + + model: str + """ + The model to use for embedding, overriding the model configured for the + attribute. + """ From 3f25ce7bb2507a7cd8ff289680737e734dc0ebea Mon Sep 17 00:00:00 2001 From: Nikhil Benesch Date: Tue, 2 Jun 2026 01:10:51 -0400 Subject: [PATCH 4/8] bump apigen (#238) --- scripts/gen | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/gen b/scripts/gen index 23fecb84..a0e66a12 100755 --- a/scripts/gen +++ b/scripts/gen @@ -4,7 +4,7 @@ set -e cd "$(dirname "$0")/.." -apigen_image=ghcr.io/turbopuffer/turbopuffer-apigen:864b25e7f396607f5fee302f6aed713afab55020 +apigen_image=ghcr.io/turbopuffer/turbopuffer-apigen:1b58f2aa9172bf7a668bf862c271b852e95a846b apigen() { if [[ "$TURBOPUFFER_DEV_APIGEN" ]]; then From 3f6123a88c2c3795f9eb4da31ef66eaa9081db18 Mon Sep 17 00:00:00 2001 From: Jan Teske Date: Tue, 2 Jun 2026 17:06:09 +0200 Subject: [PATCH 5/8] fix: reject malicious poll locations (#236) This fixes a minor security issue where the client wouldn't check the origin of the async response `Location` header before starting to poll it, allowing a man-in-the-middle to send poll requests (including API credentials) to arbitrary locations. This is a minor security issue, as such a man-in-the-middle would likely already know the API credentials from observing the initial authenticated request. The fix is to compare the poll origin with the one from the original request, and reject if they differ. --- src/turbopuffer/lib/respond_async.py | 25 +++++++++++++++--- tests/custom/test_respond_async.py | 39 ++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/turbopuffer/lib/respond_async.py b/src/turbopuffer/lib/respond_async.py index a39684e8..f0c3769d 100644 --- a/src/turbopuffer/lib/respond_async.py +++ b/src/turbopuffer/lib/respond_async.py @@ -118,14 +118,33 @@ def _respond_async_applied(response: httpx.Response) -> bool: def _extract_location(response: httpx.Response) -> str: - location: str = response.headers.get(HEADER_LOCATION, "").strip() - if not location: + raw_location: str = response.headers.get(HEADER_LOCATION, "").strip() + if not raw_location: raise APIResponseValidationError( response=response, body=response.text, message="missing 'Location' header on respond-async response", ) - return location + + orig = response.request.url + try: + # Resolve the Location against the original request URL. + location = orig.join(raw_location) + except httpx.InvalidURL as err: + raise APIResponseValidationError( + response=response, + body=response.text, + message=f"malformed 'Location' header: {raw_location!r}", + ) from err + + # Reject a Location pointing at a different origin, to prevent API key exfiltration. + if (location.scheme, location.host, location.port) != (orig.scheme, orig.host, orig.port): + raise APIResponseValidationError( + response=response, + body=response.text, + message=f"'Location' origin does not match request origin: {raw_location!r}", + ) + return str(location) class _PollError(BaseModel): diff --git a/tests/custom/test_respond_async.py b/tests/custom/test_respond_async.py index f600c044..376f92e2 100644 --- a/tests/custom/test_respond_async.py +++ b/tests/custom/test_respond_async.py @@ -360,6 +360,45 @@ async def test_async_async_applied_missing_location_header() -> None: await client.namespace("test").write(upsert_columns={"id": [1], "vector": [[0.1]]}) +BAD_LOCATIONS = [ + "https://evil.example.com/v1/ops/op-x", + "//evil.example.com/v1/ops/op-x", + "http://api.turbopuffer.com/v1/ops/op-x", + "http://host:notaport/x", +] + + +@respx.mock +@pytest.mark.parametrize("bad_location", BAD_LOCATIONS) +def test_sync_async_applied_bad_location(bad_location: str) -> None: + respx.post(f"{base_url}/v2/namespaces/test").mock( + return_value=httpx.Response( + 202, + headers={"preference-applied": "respond-async", "location": bad_location}, + ) + ) + http_client = httpx.Client(transport=httpx.HTTPTransport()) + client = Turbopuffer(base_url=base_url, api_key=api_key, http_client=http_client) + with pytest.raises(turbopuffer.APIResponseValidationError): + client.namespace("test").write(upsert_columns={"id": [1], "vector": [[0.1]]}) + + +@respx.mock +@pytest.mark.asyncio +@pytest.mark.parametrize("bad_location", BAD_LOCATIONS) +async def test_async_async_applied_bad_location(bad_location: str) -> None: + respx.post(f"{base_url}/v2/namespaces/test").mock( + return_value=httpx.Response( + 202, + headers={"preference-applied": "respond-async", "location": bad_location}, + ) + ) + http_client = httpx.AsyncClient(transport=httpx.AsyncHTTPTransport()) + async with AsyncTurbopuffer(base_url=base_url, api_key=api_key, http_client=http_client) as client: + with pytest.raises(turbopuffer.APIResponseValidationError): + await client.namespace("test").write(upsert_columns={"id": [1], "vector": [[0.1]]}) + + @respx.mock def test_sync_poll_too_many_failures() -> None: poll_url = f"{base_url}/v1/namespaces/test/operations/op-dead" From 80ab218582e9b4e78dd9fca42cdce6f2a76761cf Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 15:19:18 +0000 Subject: [PATCH 6/8] feat: openapi: spec for `rerank_by: ["RRF"]` --- .stats.yml | 6 +++--- api.md | 1 + src/turbopuffer/resources/namespaces.py | 8 ++++++++ src/turbopuffer/types/__init__.py | 1 + src/turbopuffer/types/custom.py | 2 ++ .../types/namespace_multi_query_params.py | 3 +++ src/turbopuffer/types/rrf_params.py | 14 ++++++++++++++ tests/api_resources/test_namespaces.py | 2 ++ 8 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 src/turbopuffer/types/rrf_params.py diff --git a/.stats.yml b/.stats.yml index 0600dd6d..74337dcd 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 14 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/turbopuffer-benesch/turbopuffer-a7a6fdfec853694b22f9773420ab0309a9271c7a44fb268cfdefb28cd1882c0f.yml -openapi_spec_hash: 302772c38d7fc6fb8127d302174d1f17 -config_hash: 8ea213ebc62d78cc8a79b52611c70900 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/turbopuffer-benesch/turbopuffer-ffcaae9b05de6560eb11bb0821cf4d6ebc06f1464c6d8258d79c1a0f2eda41fb.yml +openapi_spec_hash: 2ffbfcbe9c95eb73ef0be3dbef708e6a +config_hash: bab72dc9f937352c7a01a37dadd44122 diff --git a/api.md b/api.md index 90a764b1..8f6d7777 100644 --- a/api.md +++ b/api.md @@ -47,6 +47,7 @@ from turbopuffer.types import ( QueryBilling, QueryPerformance, Row, + RrfParams, SaturateParams, SparseDistanceMetric, Tokenizer, diff --git a/src/turbopuffer/resources/namespaces.py b/src/turbopuffer/resources/namespaces.py index 4f0d1098..ad7b3628 100644 --- a/src/turbopuffer/resources/namespaces.py +++ b/src/turbopuffer/resources/namespaces.py @@ -376,6 +376,7 @@ def multi_query( namespace: str | None = None, queries: Iterable[namespace_multi_query_params.Query], consistency: namespace_multi_query_params.Consistency | Omit = omit, + rerank_by: object | Omit = omit, vector_encoding: VectorEncoding | 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. @@ -390,6 +391,8 @@ def multi_query( Args: consistency: The consistency level for a query. + rerank_by: How to combine the rows returned by each sub-query into a single ranked list. + vector_encoding: The encoding to use for vectors in the response. extra_headers: Send extra headers @@ -410,6 +413,7 @@ def multi_query( { "queries": queries, "consistency": consistency, + "rerank_by": rerank_by, "vector_encoding": vector_encoding, }, namespace_multi_query_params.NamespaceMultiQueryParams, @@ -1131,6 +1135,7 @@ async def multi_query( namespace: str | None = None, queries: Iterable[namespace_multi_query_params.Query], consistency: namespace_multi_query_params.Consistency | Omit = omit, + rerank_by: object | Omit = omit, vector_encoding: VectorEncoding | 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. @@ -1145,6 +1150,8 @@ async def multi_query( Args: consistency: The consistency level for a query. + rerank_by: How to combine the rows returned by each sub-query into a single ranked list. + vector_encoding: The encoding to use for vectors in the response. extra_headers: Send extra headers @@ -1165,6 +1172,7 @@ async def multi_query( { "queries": queries, "consistency": consistency, + "rerank_by": rerank_by, "vector_encoding": vector_encoding, }, namespace_multi_query_params.NamespaceMultiQueryParams, diff --git a/src/turbopuffer/types/__init__.py b/src/turbopuffer/types/__init__.py index fbcc49ed..79a6a0d8 100644 --- a/src/turbopuffer/types/__init__.py +++ b/src/turbopuffer/types/__init__.py @@ -11,6 +11,7 @@ from .row_param import RowParam as RowParam from .tokenizer import Tokenizer as Tokenizer from .encryption import Encryption as Encryption +from .rrf_params import RrfParams as RrfParams from .limit_param import LimitParam as LimitParam from .decay_params import DecayParams as DecayParams from .embed_params import EmbedParams as EmbedParams diff --git a/src/turbopuffer/types/custom.py b/src/turbopuffer/types/custom.py index a61ebedf..e1d9f2f8 100644 --- a/src/turbopuffer/types/custom.py +++ b/src/turbopuffer/types/custom.py @@ -2,6 +2,7 @@ from typing import Any, Tuple, Union, Literal, Mapping, Sequence, TypedDict +from .rrf_params import RrfParams from .decay_params import DecayParams from .embed_params import EmbedParams from .fuzzy_params import FuzzyParams @@ -85,3 +86,4 @@ RankByAttribute, RankByAttributes, ] +RerankBy = Union[Tuple[Literal["RRF"]], Tuple[Literal["RRF"], RrfParams]] diff --git a/src/turbopuffer/types/namespace_multi_query_params.py b/src/turbopuffer/types/namespace_multi_query_params.py index 4dac2969..140fd46b 100644 --- a/src/turbopuffer/types/namespace_multi_query_params.py +++ b/src/turbopuffer/types/namespace_multi_query_params.py @@ -23,6 +23,9 @@ class NamespaceMultiQueryParams(TypedDict, total=False): consistency: Consistency """The consistency level for a query.""" + rerank_by: object + """How to combine the rows returned by each sub-query into a single ranked list.""" + vector_encoding: VectorEncoding """The encoding to use for vectors in the response.""" diff --git a/src/turbopuffer/types/rrf_params.py b/src/turbopuffer/types/rrf_params.py new file mode 100644 index 00000000..3e6c0c1a --- /dev/null +++ b/src/turbopuffer/types/rrf_params.py @@ -0,0 +1,14 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["RrfParams"] + + +class RrfParams(TypedDict, total=False): + """Configuration options for RRF.""" + + rank_constant: int + """RRF rank constant (`k`). Must be greater than zero. Defaults to `60`.""" diff --git a/tests/api_resources/test_namespaces.py b/tests/api_resources/test_namespaces.py index 610e50d4..28cd1196 100644 --- a/tests/api_resources/test_namespaces.py +++ b/tests/api_resources/test_namespaces.py @@ -299,6 +299,7 @@ def test_method_multi_query_with_all_params(self, client: Turbopuffer) -> None: } ], consistency={"level": "strong"}, + rerank_by={}, vector_encoding="float", ) assert_matches_type(NamespaceMultiQueryResponse, namespace, path=["response"]) @@ -917,6 +918,7 @@ async def test_method_multi_query_with_all_params(self, async_client: AsyncTurbo } ], consistency={"level": "strong"}, + rerank_by={}, vector_encoding="float", ) assert_matches_type(NamespaceMultiQueryResponse, namespace, path=["response"]) From 6f07a95860f5dff9b699776d7916606b5c6f7cab Mon Sep 17 00:00:00 2001 From: Jan Teske Date: Tue, 2 Jun 2026 18:59:59 +0200 Subject: [PATCH 7/8] fix: type rerank_by parameter as RerankBy (#239) Stainless leaves rerank_by typed as 'object'. Wire it to the already-generated RerankBy type so callers get a proper signature. --- src/turbopuffer/resources/namespaces.py | 6 +++--- tests/api_resources/test_namespaces.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/turbopuffer/resources/namespaces.py b/src/turbopuffer/resources/namespaces.py index ad7b3628..876edec2 100644 --- a/src/turbopuffer/resources/namespaces.py +++ b/src/turbopuffer/resources/namespaces.py @@ -33,7 +33,7 @@ ) from .._exceptions import NotFoundError from .._base_client import make_request_options -from ..types.custom import Filter, RankBy, GroupBy, AggregateBy +from ..types.custom import Filter, RankBy, GroupBy, RerankBy, AggregateBy from ..types.id_param import IDParam from ..types.row_param import RowParam from ..types.columns_param import ColumnsParam @@ -376,7 +376,7 @@ def multi_query( namespace: str | None = None, queries: Iterable[namespace_multi_query_params.Query], consistency: namespace_multi_query_params.Consistency | Omit = omit, - rerank_by: object | Omit = omit, + rerank_by: RerankBy | Omit = omit, vector_encoding: VectorEncoding | 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. @@ -1135,7 +1135,7 @@ async def multi_query( namespace: str | None = None, queries: Iterable[namespace_multi_query_params.Query], consistency: namespace_multi_query_params.Consistency | Omit = omit, - rerank_by: object | Omit = omit, + rerank_by: RerankBy | Omit = omit, vector_encoding: VectorEncoding | 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. diff --git a/tests/api_resources/test_namespaces.py b/tests/api_resources/test_namespaces.py index 28cd1196..553f47c0 100644 --- a/tests/api_resources/test_namespaces.py +++ b/tests/api_resources/test_namespaces.py @@ -299,7 +299,7 @@ def test_method_multi_query_with_all_params(self, client: Turbopuffer) -> None: } ], consistency={"level": "strong"}, - rerank_by={}, + rerank_by=("RRF",), vector_encoding="float", ) assert_matches_type(NamespaceMultiQueryResponse, namespace, path=["response"]) @@ -918,7 +918,7 @@ async def test_method_multi_query_with_all_params(self, async_client: AsyncTurbo } ], consistency={"level": "strong"}, - rerank_by={}, + rerank_by=("RRF",), vector_encoding="float", ) assert_matches_type(NamespaceMultiQueryResponse, namespace, path=["response"]) From bf7ee86b27e89f5c6a6464453991d6a6f8736986 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 17:13:09 +0000 Subject: [PATCH 8/8] release: 2.3.0-alpha.1 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 21 +++++++++++++++++++++ README.md | 4 ++-- pyproject.toml | 2 +- src/turbopuffer/_version.py | 2 +- 5 files changed, 26 insertions(+), 5 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index bfc26f9c..b6fd98c0 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.2.0" + ".": "2.3.0-alpha.1" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index d4dff2c7..a40c3a32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## 2.3.0-alpha.1 (2026-06-02) + +Full Changelog: [v2.2.0...v2.3.0-alpha.1](https://github.com/turbopuffer/turbopuffer-python/compare/v2.2.0...v2.3.0-alpha.1) + +### Features + +* openapi: spec for `rerank_by: ["RRF"]` ([80ab218](https://github.com/turbopuffer/turbopuffer-python/commit/80ab218582e9b4e78dd9fca42cdce6f2a76761cf)) +* rename /docs/auth to /docs/overview ([1340eb1](https://github.com/turbopuffer/turbopuffer-python/commit/1340eb172500c78d9ce73bfe8ce557bcdb40c4d7)) +* spec: add SDK support for native embedding ([ca71729](https://github.com/turbopuffer/turbopuffer-python/commit/ca71729755294ed2eb073453e33baa8c89548063)) + + +### Bug Fixes + +* reject malicious poll locations ([#236](https://github.com/turbopuffer/turbopuffer-python/issues/236)) ([3f6123a](https://github.com/turbopuffer/turbopuffer-python/commit/3f6123a88c2c3795f9eb4da31ef66eaa9081db18)) +* type rerank_by parameter as RerankBy ([#239](https://github.com/turbopuffer/turbopuffer-python/issues/239)) ([6f07a95](https://github.com/turbopuffer/turbopuffer-python/commit/6f07a95860f5dff9b699776d7916606b5c6f7cab)) + + +### Chores + +* fix API docs links ([#235](https://github.com/turbopuffer/turbopuffer-python/issues/235)) ([204b46b](https://github.com/turbopuffer/turbopuffer-python/commit/204b46bf15c5694bdd38339b8471c46e16e697a0)) + ## 2.2.0 (2026-05-28) Full Changelog: [v2.1.0...v2.2.0](https://github.com/turbopuffer/turbopuffer-python/compare/v2.1.0...v2.2.0) diff --git a/README.md b/README.md index e5637be4..70804615 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ The HTTP API documentation can be found at [turbopuffer.com/docs/overview](https ```sh # install from PyPI -pip install turbopuffer +pip install --pre turbopuffer ``` ## Usage @@ -128,7 +128,7 @@ You can enable this by installing `aiohttp`: ```sh # install from PyPI -pip install turbopuffer[aiohttp] +pip install --pre turbopuffer[aiohttp] ``` Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: diff --git a/pyproject.toml b/pyproject.toml index c819bb1e..c9861379 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "turbopuffer" -version = "2.2.0" +version = "2.3.0-alpha.1" description = "The official Python library for the turbopuffer API" dynamic = ["readme"] license = "MIT" diff --git a/src/turbopuffer/_version.py b/src/turbopuffer/_version.py index e9a56e32..e12dcfdc 100644 --- a/src/turbopuffer/_version.py +++ b/src/turbopuffer/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "turbopuffer" -__version__ = "2.2.0" # x-release-please-version +__version__ = "2.3.0-alpha.1" # x-release-please-version