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/.stats.yml b/.stats.yml index cc6bfd14..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-6717a82f2b22fd2b55bce7acb7d5e9a694c51132a5522626d39684bff6e0cb83.yml -openapi_spec_hash: 647e6105fbcaba7eaed15f1859f1c463 -config_hash: 3917af929e17def75be204eb7e4000fa +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/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 2db55f3a..70804615 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,13 +20,13 @@ 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 ```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()`: @@ -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: diff --git a/api.md b/api.md index cdce2855..8f6d7777 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, @@ -44,6 +47,7 @@ from turbopuffer.types import ( QueryBilling, QueryPerformance, Row, + RrfParams, SaturateParams, SparseDistanceMetric, Tokenizer, 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/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 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 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/src/turbopuffer/resources/namespaces.py b/src/turbopuffer/resources/namespaces.py index 4f0d1098..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,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: 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. @@ -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: 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. @@ -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 421e358c..79a6a0d8 100644 --- a/src/turbopuffer/types/__init__.py +++ b/src/turbopuffer/types/__init__.py @@ -11,8 +11,10 @@ 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 from .fuzzy_params import FuzzyParams as FuzzyParams from .vector_param import VectorParam as VectorParam from .columns_param import ColumnsParam as ColumnsParam @@ -20,6 +22,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 +35,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 +53,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..e1d9f2f8 100644 --- a/src/turbopuffer/types/custom.py +++ b/src/turbopuffer/types/custom.py @@ -2,7 +2,9 @@ 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 from .saturate_params import SaturateParams from .bm25_clause_params import Bm25ClauseParams @@ -11,7 +13,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 +54,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,9 +78,12 @@ RankByAttributes = Sequence[RankByAttribute] RankBy = Union[ RankByAnn, + RankByAnnExpr, RankByKnn, + RankByKnnExpr, RankBySparseKnn, RankByText, RankByAttribute, RankByAttributes, ] +RerankBy = Union[Tuple[Literal["RRF"]], Tuple[Literal["RRF"], RrfParams]] 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. + """ 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..553f47c0 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=("RRF",), 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=("RRF",), vector_encoding="float", ) assert_matches_type(NamespaceMultiQueryResponse, namespace, path=["response"]) 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"