diff --git a/.release-please-manifest.json b/.release-please-manifest.json index e7562934..0c2ecec6 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.19.0" + ".": "0.20.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index c6bec7fc..392f60c3 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 49 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/mixedbread%2Fmixedbread-d45a3a3119fa9a3db2a6cae3c6d376f99fb874ed359c369d9b9531fdea55dcec.yml -openapi_spec_hash: aedb38c67ac4c4b9ee79d130ddeb583a +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/mixedbread%2Fmixedbread-e8f008047e9aa7f8824bbf5c8b3f338ce1f79b4c81f2637a5e4be1530ff45c9f.yml +openapi_spec_hash: 27f45c14fedc15710f730e037f0694cf config_hash: 810d9712d3d0d6a1f50d71a25511d8a7 diff --git a/CHANGELOG.md b/CHANGELOG.md index e705664b..181f58f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## 0.20.0 (2025-07-22) + +Full Changelog: [v0.19.0...v0.20.0](https://github.com/mixedbread-ai/mixedbread-python/compare/v0.19.0...v0.20.0) + +### Features + +* **api:** api update ([b49a79c](https://github.com/mixedbread-ai/mixedbread-python/commit/b49a79cf86930dae1e2f5b2732f7c78fb23d6248)) +* clean up environment call outs ([d0c1c5e](https://github.com/mixedbread-ai/mixedbread-python/commit/d0c1c5e3a871ec7f89a4ec1d0d29e4b4ba97f6e8)) + + +### Bug Fixes + +* **parsing:** ignore empty metadata ([697e64e](https://github.com/mixedbread-ai/mixedbread-python/commit/697e64e4276c78d19075393ce2d605c401c9026a)) + + +### Chores + +* **internal:** codegen related update ([b14dd64](https://github.com/mixedbread-ai/mixedbread-python/commit/b14dd6477262cb4611eca7d5a67aaad8f3694088)) +* **types:** rebuild Pydantic models after all types are defined ([1a5b6a5](https://github.com/mixedbread-ai/mixedbread-python/commit/1a5b6a5fd8b9aada359878cdffbb71e6bd888a5d)) + ## 0.19.0 (2025-07-17) Full Changelog: [v0.18.0...v0.19.0](https://github.com/mixedbread-ai/mixedbread-python/compare/v0.18.0...v0.19.0) diff --git a/README.md b/README.md index ef53e23e..6b66d10d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Mixedbread API Python SDK API library -[![PyPI version]()](https://pypi.org/project/mixedbread/) + +[![PyPI version](https://img.shields.io/pypi/v/mixedbread.svg?label=pypi%20(stable))](https://pypi.org/project/mixedbread/) The Mixedbread API Python SDK library provides convenient access to the Mixedbread REST API from any Python 3.8+ application. The library includes type definitions for all request params and response fields, @@ -82,7 +83,6 @@ pip install mixedbread[aiohttp] Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: ```python -import os import asyncio from mixedbread import DefaultAioHttpClient from mixedbread import AsyncMixedbread @@ -90,7 +90,7 @@ from mixedbread import AsyncMixedbread async def main() -> None: async with AsyncMixedbread( - api_key=os.environ.get("MXBAI_API_KEY"), # This is the default and can be omitted + api_key="My API Key", http_client=DefaultAioHttpClient(), ) as client: vector_store = await client.vector_stores.create() diff --git a/pyproject.toml b/pyproject.toml index 5a7c87c0..0ef339ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mixedbread" -version = "0.19.0" +version = "0.20.0" description = "The official Python library for the Mixedbread API" dynamic = ["readme"] license = "Apache-2.0" @@ -24,6 +24,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: MacOS", @@ -38,7 +39,7 @@ Homepage = "https://github.com/mixedbread-ai/mixedbread-python" Repository = "https://github.com/mixedbread-ai/mixedbread-python" [project.optional-dependencies] -aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.6"] +aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.8"] [tool.rye] managed = true diff --git a/requirements-dev.lock b/requirements-dev.lock index d37564b7..75170ccd 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -48,9 +48,9 @@ filelock==3.12.4 frozenlist==1.6.2 # via aiohttp # via aiosignal -h11==0.14.0 +h11==0.16.0 # via httpcore -httpcore==1.0.2 +httpcore==1.0.9 # via httpx httpx==0.28.1 # via httpx-aiohttp diff --git a/requirements.lock b/requirements.lock index e7c74727..93fffc81 100644 --- a/requirements.lock +++ b/requirements.lock @@ -36,9 +36,9 @@ exceptiongroup==1.2.2 frozenlist==1.6.2 # via aiohttp # via aiosignal -h11==0.14.0 +h11==0.16.0 # via httpcore -httpcore==1.0.2 +httpcore==1.0.9 # via httpx httpx==0.28.1 # via httpx-aiohttp diff --git a/src/mixedbread/_base_client.py b/src/mixedbread/_base_client.py index 8ec40870..40ee1387 100644 --- a/src/mixedbread/_base_client.py +++ b/src/mixedbread/_base_client.py @@ -529,6 +529,15 @@ def _build_request( # work around https://github.com/encode/httpx/discussions/2880 kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")} + is_body_allowed = options.method.lower() != "get" + + if is_body_allowed: + kwargs["json"] = json_data if is_given(json_data) else None + kwargs["files"] = files + else: + headers.pop("Content-Type", None) + kwargs.pop("data", None) + # TODO: report this error to httpx return self._client.build_request( # pyright: ignore[reportUnknownMemberType] headers=headers, @@ -540,8 +549,6 @@ def _build_request( # so that passing a `TypedDict` doesn't cause an error. # https://github.com/microsoft/pyright/issues/3526#event-6715453066 params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None, - json=json_data if is_given(json_data) else None, - files=files, **kwargs, ) diff --git a/src/mixedbread/_models.py b/src/mixedbread/_models.py index 4f214980..ffcbf67b 100644 --- a/src/mixedbread/_models.py +++ b/src/mixedbread/_models.py @@ -2,9 +2,10 @@ import os import inspect -from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, cast +from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast from datetime import date, datetime from typing_extensions import ( + List, Unpack, Literal, ClassVar, @@ -366,7 +367,7 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object: if type_ is None: raise RuntimeError(f"Unexpected field type is None for {key}") - return construct_type(value=value, type_=type_) + return construct_type(value=value, type_=type_, metadata=getattr(field, "metadata", None)) def is_basemodel(type_: type) -> bool: @@ -420,7 +421,7 @@ def construct_type_unchecked(*, value: object, type_: type[_T]) -> _T: return cast(_T, construct_type(value=value, type_=type_)) -def construct_type(*, value: object, type_: object) -> object: +def construct_type(*, value: object, type_: object, metadata: Optional[List[Any]] = None) -> object: """Loose coercion to the expected type with construction of nested values. If the given value does not match the expected type then it is returned as-is. @@ -438,8 +439,10 @@ def construct_type(*, value: object, type_: object) -> object: type_ = type_.__value__ # type: ignore[unreachable] # unwrap `Annotated[T, ...]` -> `T` - if is_annotated_type(type_): - meta: tuple[Any, ...] = get_args(type_)[1:] + if metadata is not None and len(metadata) > 0: + meta: tuple[Any, ...] = tuple(metadata) + elif is_annotated_type(type_): + meta = get_args(type_)[1:] type_ = extract_type_arg(type_, 0) else: meta = tuple() diff --git a/src/mixedbread/_version.py b/src/mixedbread/_version.py index 54e58eec..0c912dc1 100644 --- a/src/mixedbread/_version.py +++ b/src/mixedbread/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "mixedbread" -__version__ = "0.19.0" # x-release-please-version +__version__ = "0.20.0" # x-release-please-version diff --git a/src/mixedbread/resources/data_sources/data_sources.py b/src/mixedbread/resources/data_sources/data_sources.py index dd533886..3579b305 100644 --- a/src/mixedbread/resources/data_sources/data_sources.py +++ b/src/mixedbread/resources/data_sources/data_sources.py @@ -3,17 +3,11 @@ from __future__ import annotations from typing import Optional -from typing_extensions import overload +from typing_extensions import Literal, overload import httpx -from ...types import ( - Oauth2Params, - DataSourceType, - data_source_list_params, - data_source_create_params, - data_source_update_params, -) +from ...types import Oauth2Params, data_source_list_params, data_source_create_params, data_source_update_params from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven from ..._utils import required_args, maybe_transform, async_maybe_transform from ..._compat import cached_property @@ -36,7 +30,6 @@ from ..._base_client import AsyncPaginator, make_request_options from ...types.data_source import DataSource from ...types.oauth2_params import Oauth2Params -from ...types.data_source_type import DataSourceType from ...types.data_source_delete_response import DataSourceDeleteResponse __all__ = ["DataSourcesResource", "AsyncDataSourcesResource"] @@ -70,7 +63,7 @@ def with_streaming_response(self) -> DataSourcesResourceWithStreamingResponse: def create( self, *, - type: DataSourceType | NotGiven = NOT_GIVEN, + type: Literal["notion"] | NotGiven = NOT_GIVEN, name: str, metadata: object | NotGiven = NOT_GIVEN, auth_params: Optional[data_source_create_params.NotionDataSourceAuthParams] | NotGiven = NOT_GIVEN, @@ -112,7 +105,7 @@ def create( def create( self, *, - type: DataSourceType | NotGiven = NOT_GIVEN, + type: Literal["linear"] | NotGiven = NOT_GIVEN, name: str, metadata: object | NotGiven = NOT_GIVEN, auth_params: Optional[Oauth2Params] | NotGiven = NOT_GIVEN, @@ -153,7 +146,7 @@ def create( def create( self, *, - type: DataSourceType | NotGiven = NOT_GIVEN, + type: Literal["notion"] | Literal["linear"] | NotGiven = NOT_GIVEN, name: str, metadata: object | NotGiven = NOT_GIVEN, auth_params: Optional[data_source_create_params.NotionDataSourceAuthParams] @@ -227,7 +220,7 @@ def update( self, data_source_id: str, *, - type: DataSourceType | NotGiven = NOT_GIVEN, + type: Literal["notion"] | NotGiven = NOT_GIVEN, name: str, metadata: object | NotGiven = NOT_GIVEN, auth_params: Optional[data_source_update_params.NotionDataSourceAuthParams] | NotGiven = NOT_GIVEN, @@ -274,7 +267,7 @@ def update( self, data_source_id: str, *, - type: DataSourceType | NotGiven = NOT_GIVEN, + type: Literal["linear"] | NotGiven = NOT_GIVEN, name: str, metadata: object | NotGiven = NOT_GIVEN, auth_params: Optional[Oauth2Params] | NotGiven = NOT_GIVEN, @@ -320,7 +313,7 @@ def update( self, data_source_id: str, *, - type: DataSourceType | NotGiven = NOT_GIVEN, + type: Literal["notion"] | Literal["linear"] | NotGiven = NOT_GIVEN, name: str, metadata: object | NotGiven = NOT_GIVEN, auth_params: Optional[data_source_update_params.NotionDataSourceAuthParams] @@ -477,7 +470,7 @@ def with_streaming_response(self) -> AsyncDataSourcesResourceWithStreamingRespon async def create( self, *, - type: DataSourceType | NotGiven = NOT_GIVEN, + type: Literal["notion"] | NotGiven = NOT_GIVEN, name: str, metadata: object | NotGiven = NOT_GIVEN, auth_params: Optional[data_source_create_params.NotionDataSourceAuthParams] | NotGiven = NOT_GIVEN, @@ -519,7 +512,7 @@ async def create( async def create( self, *, - type: DataSourceType | NotGiven = NOT_GIVEN, + type: Literal["linear"] | NotGiven = NOT_GIVEN, name: str, metadata: object | NotGiven = NOT_GIVEN, auth_params: Optional[Oauth2Params] | NotGiven = NOT_GIVEN, @@ -560,7 +553,7 @@ async def create( async def create( self, *, - type: DataSourceType | NotGiven = NOT_GIVEN, + type: Literal["notion"] | Literal["linear"] | NotGiven = NOT_GIVEN, name: str, metadata: object | NotGiven = NOT_GIVEN, auth_params: Optional[data_source_create_params.NotionDataSourceAuthParams] @@ -634,7 +627,7 @@ async def update( self, data_source_id: str, *, - type: DataSourceType | NotGiven = NOT_GIVEN, + type: Literal["notion"] | NotGiven = NOT_GIVEN, name: str, metadata: object | NotGiven = NOT_GIVEN, auth_params: Optional[data_source_update_params.NotionDataSourceAuthParams] | NotGiven = NOT_GIVEN, @@ -681,7 +674,7 @@ async def update( self, data_source_id: str, *, - type: DataSourceType | NotGiven = NOT_GIVEN, + type: Literal["linear"] | NotGiven = NOT_GIVEN, name: str, metadata: object | NotGiven = NOT_GIVEN, auth_params: Optional[Oauth2Params] | NotGiven = NOT_GIVEN, @@ -727,7 +720,7 @@ async def update( self, data_source_id: str, *, - type: DataSourceType | NotGiven = NOT_GIVEN, + type: Literal["notion"] | Literal["linear"] | NotGiven = NOT_GIVEN, name: str, metadata: object | NotGiven = NOT_GIVEN, auth_params: Optional[data_source_update_params.NotionDataSourceAuthParams] diff --git a/src/mixedbread/types/__init__.py b/src/mixedbread/types/__init__.py index 5fead10f..aca3e061 100644 --- a/src/mixedbread/types/__init__.py +++ b/src/mixedbread/types/__init__.py @@ -2,6 +2,8 @@ from __future__ import annotations +from . import shared +from .. import _compat from .shared import Usage as Usage, SearchFilter as SearchFilter, SearchFilterCondition as SearchFilterCondition from .api_key import APIKey as APIKey from .embedding import Embedding as Embedding @@ -56,3 +58,12 @@ from .vector_store_question_answering_response import ( VectorStoreQuestionAnsweringResponse as VectorStoreQuestionAnsweringResponse, ) + +# Rebuild cyclical models only after all modules are imported. +# This ensures that, when building the deferred (due to cyclical references) model schema, +# Pydantic can resolve the necessary references. +# See: https://github.com/pydantic/pydantic/issues/11250 for more context. +if _compat.PYDANTIC_V2: + shared.search_filter.SearchFilter.model_rebuild(_parent_namespace_depth=0) +else: + shared.search_filter.SearchFilter.update_forward_refs() # type: ignore diff --git a/src/mixedbread/types/data_source_create_params.py b/src/mixedbread/types/data_source_create_params.py index caa23d67..9cc9738a 100644 --- a/src/mixedbread/types/data_source_create_params.py +++ b/src/mixedbread/types/data_source_create_params.py @@ -6,7 +6,6 @@ from typing_extensions import Literal, Required, TypeAlias, TypedDict from .oauth2_params import Oauth2Params -from .data_source_type import DataSourceType __all__ = [ "DataSourceCreateParams", @@ -18,7 +17,7 @@ class NotionDataSource(TypedDict, total=False): - type: DataSourceType + type: Literal["notion"] """The type of data source to create""" name: Required[str] @@ -45,7 +44,7 @@ class NotionDataSourceAuthParamsAPIKeyCreateOrUpdateParams(TypedDict, total=Fals class LinearDataSource(TypedDict, total=False): - type: DataSourceType + type: Literal["linear"] """The type of data source to create""" name: Required[str] diff --git a/src/mixedbread/types/data_source_update_params.py b/src/mixedbread/types/data_source_update_params.py index 6c283f81..9aa8872b 100644 --- a/src/mixedbread/types/data_source_update_params.py +++ b/src/mixedbread/types/data_source_update_params.py @@ -6,7 +6,6 @@ from typing_extensions import Literal, Required, TypeAlias, TypedDict from .oauth2_params import Oauth2Params -from .data_source_type import DataSourceType __all__ = [ "DataSourceUpdateParams", @@ -18,7 +17,7 @@ class NotionDataSource(TypedDict, total=False): - type: DataSourceType + type: Literal["notion"] """The type of data source to create""" name: Required[str] @@ -45,7 +44,7 @@ class NotionDataSourceAuthParamsAPIKeyCreateOrUpdateParams(TypedDict, total=Fals class LinearDataSource(TypedDict, total=False): - type: DataSourceType + type: Literal["linear"] """The type of data source to create""" name: Required[str] diff --git a/src/mixedbread/types/linear_data_source_param.py b/src/mixedbread/types/linear_data_source_param.py index 9dbe8904..8956d2e3 100644 --- a/src/mixedbread/types/linear_data_source_param.py +++ b/src/mixedbread/types/linear_data_source_param.py @@ -3,16 +3,15 @@ from __future__ import annotations from typing import Optional -from typing_extensions import Required, TypedDict +from typing_extensions import Literal, Required, TypedDict from .oauth2_params import Oauth2Params -from .data_source_type import DataSourceType __all__ = ["LinearDataSourceParam"] class LinearDataSourceParam(TypedDict, total=False): - type: DataSourceType + type: Literal["linear"] """The type of data source to create""" name: Required[str] diff --git a/src/mixedbread/types/notion_data_source_param.py b/src/mixedbread/types/notion_data_source_param.py index 914c502f..65d12b2e 100644 --- a/src/mixedbread/types/notion_data_source_param.py +++ b/src/mixedbread/types/notion_data_source_param.py @@ -6,7 +6,6 @@ from typing_extensions import Literal, Required, TypeAlias, TypedDict from .oauth2_params import Oauth2Params -from .data_source_type import DataSourceType __all__ = ["NotionDataSourceParam", "AuthParams", "AuthParamsAPIKeyCreateOrUpdateParams"] @@ -22,7 +21,7 @@ class AuthParamsAPIKeyCreateOrUpdateParams(TypedDict, total=False): class NotionDataSourceParam(TypedDict, total=False): - type: DataSourceType + type: Literal["notion"] """The type of data source to create""" name: Required[str] diff --git a/src/mixedbread/types/shared/search_filter.py b/src/mixedbread/types/shared/search_filter.py index 4835d18d..579ae162 100644 --- a/src/mixedbread/types/shared/search_filter.py +++ b/src/mixedbread/types/shared/search_filter.py @@ -36,9 +36,3 @@ class SearchFilter(BaseModel): none: Optional[List[NoneType]] = None """List of conditions or filters to be NOTed""" - - -if PYDANTIC_V2: - SearchFilter.model_rebuild() -else: - SearchFilter.update_forward_refs() # type: ignore diff --git a/tests/api_resources/test_data_sources.py b/tests/api_resources/test_data_sources.py index 7ecf2ff1..5cb9991e 100644 --- a/tests/api_resources/test_data_sources.py +++ b/tests/api_resources/test_data_sources.py @@ -72,7 +72,7 @@ def test_method_create_overload_2(self, client: Mixedbread) -> None: @parametrize def test_method_create_with_all_params_overload_2(self, client: Mixedbread) -> None: data_source = client.data_sources.create( - type="notion", + type="linear", name="name", metadata={}, auth_params={"type": "oauth2"}, @@ -206,7 +206,7 @@ def test_method_update_overload_2(self, client: Mixedbread) -> None: def test_method_update_with_all_params_overload_2(self, client: Mixedbread) -> None: data_source = client.data_sources.update( data_source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - type="notion", + type="linear", name="name", metadata={}, auth_params={"type": "oauth2"}, @@ -377,7 +377,7 @@ async def test_method_create_overload_2(self, async_client: AsyncMixedbread) -> @parametrize async def test_method_create_with_all_params_overload_2(self, async_client: AsyncMixedbread) -> None: data_source = await async_client.data_sources.create( - type="notion", + type="linear", name="name", metadata={}, auth_params={"type": "oauth2"}, @@ -511,7 +511,7 @@ async def test_method_update_overload_2(self, async_client: AsyncMixedbread) -> async def test_method_update_with_all_params_overload_2(self, async_client: AsyncMixedbread) -> None: data_source = await async_client.data_sources.update( data_source_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", - type="notion", + type="linear", name="name", metadata={}, auth_params={"type": "oauth2"}, diff --git a/tests/test_client.py b/tests/test_client.py index 8f60c0c0..81aac0d0 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -464,7 +464,7 @@ def test_request_extra_query(self) -> None: def test_multipart_repeating_array(self, client: Mixedbread) -> None: request = client._build_request( FinalRequestOptions.construct( - method="get", + method="post", url="/foo", headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"}, json_data={"array": ["foo", "bar"]}, @@ -1283,7 +1283,7 @@ def test_request_extra_query(self) -> None: def test_multipart_repeating_array(self, async_client: AsyncMixedbread) -> None: request = async_client._build_request( FinalRequestOptions.construct( - method="get", + method="post", url="/foo", headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"}, json_data={"array": ["foo", "bar"]}, diff --git a/tests/test_models.py b/tests/test_models.py index 7f19dc8b..27250fa2 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -889,3 +889,48 @@ class ModelB(BaseModel): ) assert isinstance(m, ModelB) + + +def test_nested_discriminated_union() -> None: + class InnerType1(BaseModel): + type: Literal["type_1"] + + class InnerModel(BaseModel): + inner_value: str + + class InnerType2(BaseModel): + type: Literal["type_2"] + some_inner_model: InnerModel + + class Type1(BaseModel): + base_type: Literal["base_type_1"] + value: Annotated[ + Union[ + InnerType1, + InnerType2, + ], + PropertyInfo(discriminator="type"), + ] + + class Type2(BaseModel): + base_type: Literal["base_type_2"] + + T = Annotated[ + Union[ + Type1, + Type2, + ], + PropertyInfo(discriminator="base_type"), + ] + + model = construct_type( + type_=T, + value={ + "base_type": "base_type_1", + "value": { + "type": "type_2", + }, + }, + ) + assert isinstance(model, Type1) + assert isinstance(model.value, InnerType2)