From 267133097ede5f0980c2479f41d2b1627baa9205 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 22 Jul 2025 02:16:34 +0000 Subject: [PATCH 1/9] fix(parsing): ignore empty metadata --- src/lmnt/_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lmnt/_models.py b/src/lmnt/_models.py index 528d568..ffcbf67 100644 --- a/src/lmnt/_models.py +++ b/src/lmnt/_models.py @@ -439,7 +439,7 @@ def construct_type(*, value: object, type_: object, metadata: Optional[List[Any] type_ = type_.__value__ # type: ignore[unreachable] # unwrap `Annotated[T, ...]` -> `T` - if metadata is not None: + if metadata is not None and len(metadata) > 0: meta: tuple[Any, ...] = tuple(metadata) elif is_annotated_type(type_): meta = get_args(type_)[1:] From e652a624a99ba3c5c198aa00a198b6d0a4f69283 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 02:19:59 +0000 Subject: [PATCH 2/9] fix(parsing): parse extra field types --- src/lmnt/_models.py | 25 +++++++++++++++++++++++-- tests/test_models.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/lmnt/_models.py b/src/lmnt/_models.py index ffcbf67..b8387ce 100644 --- a/src/lmnt/_models.py +++ b/src/lmnt/_models.py @@ -208,14 +208,18 @@ def construct( # pyright: ignore[reportIncompatibleMethodOverride] else: fields_values[name] = field_get_default(field) + extra_field_type = _get_extra_fields_type(__cls) + _extra = {} for key, value in values.items(): if key not in model_fields: + parsed = construct_type(value=value, type_=extra_field_type) if extra_field_type is not None else value + if PYDANTIC_V2: - _extra[key] = value + _extra[key] = parsed else: _fields_set.add(key) - fields_values[key] = value + fields_values[key] = parsed object.__setattr__(m, "__dict__", fields_values) @@ -370,6 +374,23 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object: return construct_type(value=value, type_=type_, metadata=getattr(field, "metadata", None)) +def _get_extra_fields_type(cls: type[pydantic.BaseModel]) -> type | None: + if not PYDANTIC_V2: + # TODO + return None + + schema = cls.__pydantic_core_schema__ + if schema["type"] == "model": + fields = schema["schema"] + if fields["type"] == "model-fields": + extras = fields.get("extras_schema") + if extras and "cls" in extras: + # mypy can't narrow the type + return extras["cls"] # type: ignore[no-any-return] + + return None + + def is_basemodel(type_: type) -> bool: """Returns whether or not the given type is either a `BaseModel` or a union of `BaseModel`""" if is_union(type_): diff --git a/tests/test_models.py b/tests/test_models.py index 6c27337..e5f1915 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,5 +1,5 @@ import json -from typing import Any, Dict, List, Union, Optional, cast +from typing import TYPE_CHECKING, Any, Dict, List, Union, Optional, cast from datetime import datetime, timezone from typing_extensions import Literal, Annotated, TypeAliasType @@ -934,3 +934,30 @@ class Type2(BaseModel): ) assert isinstance(model, Type1) assert isinstance(model.value, InnerType2) + + +@pytest.mark.skipif(not PYDANTIC_V2, reason="this is only supported in pydantic v2 for now") +def test_extra_properties() -> None: + class Item(BaseModel): + prop: int + + class Model(BaseModel): + __pydantic_extra__: Dict[str, Item] = Field(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + other: str + + if TYPE_CHECKING: + + def __getattr__(self, attr: str) -> Item: ... + + model = construct_type( + type_=Model, + value={ + "a": {"prop": 1}, + "other": "foo", + }, + ) + assert isinstance(model, Model) + assert model.a.prop == 1 + assert isinstance(model.a, Item) + assert model.other == "foo" From 9d450b1915dbb53698e1aec84b8fee4d485fee86 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 25 Jul 2025 04:40:30 +0000 Subject: [PATCH 3/9] chore(project): add settings file for vscode --- .gitignore | 1 - .vscode/settings.json | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore index 8779740..95ceb18 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ .prism.log -.vscode _dev __pycache__ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5b01030 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.analysis.importFormat": "relative", +} From 950ce695592980163b6e4e05ef39cbbdc6364c11 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 27 Jul 2025 22:43:29 +0000 Subject: [PATCH 4/9] Update sync state to ae28d8f5628e15e410e901572c6c1cd50502f6e7 --- .sync_state | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.sync_state b/.sync_state index 2471320..d150d92 100644 --- a/.sync_state +++ b/.sync_state @@ -1 +1 @@ -1d2c8905b08c612264d01f337870cd7465ad546e +ae28d8f5628e15e410e901572c6c1cd50502f6e7 From f0ef8801046aa0ccad2eb44e67a6a44541d3f09b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 23:24:41 +0000 Subject: [PATCH 5/9] feat(api): api update --- .stats.yml | 4 ++-- src/lmnt/resources/speech.py | 12 ++++++++++++ src/lmnt/types/speech_convert_params.py | 2 ++ src/lmnt/types/speech_generate_detailed_params.py | 2 ++ src/lmnt/types/speech_generate_params.py | 2 ++ 5 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index e54602e..e335795 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 9 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/lmnt-kaikato-aryp6r%2Flmnt-com-bb5e3ea3764ab85fa5197dccdc44d6761afd3527e2e7357e151855321df90f11.yml -openapi_spec_hash: de11240efa6269c031db88972128b35d +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/lmnt-kaikato-aryp6r%2Flmnt-com-cd6e35232b72c91b7c58a77e81bc12247f69f118ebfe24ee5b6bad5d59683ff6.yml +openapi_spec_hash: 657b7065d43bc0c630785b37195ff1e6 config_hash: ad76a808facacf5f53e58d591653bac6 diff --git a/src/lmnt/resources/speech.py b/src/lmnt/resources/speech.py index 64d6e5a..9fe3927 100644 --- a/src/lmnt/resources/speech.py +++ b/src/lmnt/resources/speech.py @@ -61,6 +61,7 @@ def convert( format: Literal["aac", "mp3", "raw", "ulaw", "wav", "webm"] | NotGiven = NOT_GIVEN, language: Literal[ "auto", + "ar", "de", "en", "es", @@ -78,6 +79,7 @@ def convert( "th", "tr", "uk", + "ur", "vi", "zh", ] @@ -163,6 +165,7 @@ def generate( format: Literal["aac", "mp3", "raw", "ulaw", "wav", "webm"] | NotGiven = NOT_GIVEN, language: Literal[ "auto", + "ar", "de", "en", "es", @@ -180,6 +183,7 @@ def generate( "th", "tr", "uk", + "ur", "vi", "zh", ] @@ -286,6 +290,7 @@ def generate_detailed( format: Literal["aac", "mp3", "raw", "ulaw", "wav", "webm"] | NotGiven = NOT_GIVEN, language: Literal[ "auto", + "ar", "de", "en", "es", @@ -303,6 +308,7 @@ def generate_detailed( "th", "tr", "uk", + "ur", "vi", "zh", ] @@ -435,6 +441,7 @@ async def convert( format: Literal["aac", "mp3", "raw", "ulaw", "wav", "webm"] | NotGiven = NOT_GIVEN, language: Literal[ "auto", + "ar", "de", "en", "es", @@ -452,6 +459,7 @@ async def convert( "th", "tr", "uk", + "ur", "vi", "zh", ] @@ -537,6 +545,7 @@ async def generate( format: Literal["aac", "mp3", "raw", "ulaw", "wav", "webm"] | NotGiven = NOT_GIVEN, language: Literal[ "auto", + "ar", "de", "en", "es", @@ -554,6 +563,7 @@ async def generate( "th", "tr", "uk", + "ur", "vi", "zh", ] @@ -660,6 +670,7 @@ async def generate_detailed( format: Literal["aac", "mp3", "raw", "ulaw", "wav", "webm"] | NotGiven = NOT_GIVEN, language: Literal[ "auto", + "ar", "de", "en", "es", @@ -677,6 +688,7 @@ async def generate_detailed( "th", "tr", "uk", + "ur", "vi", "zh", ] diff --git a/src/lmnt/types/speech_convert_params.py b/src/lmnt/types/speech_convert_params.py index abda4d3..7626934 100644 --- a/src/lmnt/types/speech_convert_params.py +++ b/src/lmnt/types/speech_convert_params.py @@ -46,6 +46,7 @@ class SpeechConvertParams(TypedDict, total=False): language: Literal[ "auto", + "ar", "de", "en", "es", @@ -63,6 +64,7 @@ class SpeechConvertParams(TypedDict, total=False): "th", "tr", "uk", + "ur", "vi", "zh", ] diff --git a/src/lmnt/types/speech_generate_detailed_params.py b/src/lmnt/types/speech_generate_detailed_params.py index 19cd742..6bb9047 100644 --- a/src/lmnt/types/speech_generate_detailed_params.py +++ b/src/lmnt/types/speech_generate_detailed_params.py @@ -40,6 +40,7 @@ class SpeechGenerateDetailedParams(TypedDict, total=False): language: Literal[ "auto", + "ar", "de", "en", "es", @@ -57,6 +58,7 @@ class SpeechGenerateDetailedParams(TypedDict, total=False): "th", "tr", "uk", + "ur", "vi", "zh", ] diff --git a/src/lmnt/types/speech_generate_params.py b/src/lmnt/types/speech_generate_params.py index abf9f05..71c9ee1 100644 --- a/src/lmnt/types/speech_generate_params.py +++ b/src/lmnt/types/speech_generate_params.py @@ -40,6 +40,7 @@ class SpeechGenerateParams(TypedDict, total=False): language: Literal[ "auto", + "ar", "de", "en", "es", @@ -57,6 +58,7 @@ class SpeechGenerateParams(TypedDict, total=False): "th", "tr", "uk", + "ur", "vi", "zh", ] From b3891542ec061e7b75fefb625f59d6263c9c4c8a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 29 Jul 2025 00:39:26 +0000 Subject: [PATCH 6/9] Update sync state to 71a92b7562550ecaa0bc41ef22decde882221414 --- .sync_state | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.sync_state b/.sync_state index d150d92..52b6da1 100644 --- a/.sync_state +++ b/.sync_state @@ -1 +1 @@ -ae28d8f5628e15e410e901572c6c1cd50502f6e7 +71a92b7562550ecaa0bc41ef22decde882221414 From c9cedc017f3ac0f39430d877c7b1b229b3bc0635 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 30 Jul 2025 22:50:03 +0000 Subject: [PATCH 7/9] release: v2.1.0 --- CHANGELOG.md | 11 +++++++++++ pyproject.toml | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65322a4..019f0ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,15 @@ # 2.0.0 + +## v2.1.0 (2025-07-30) + +Full Changelog: [v2.0.0...v2.1.0](https://github.com/lmnt-com/lmnt-python/compare/v2.0.0...v2.1.0) + + ### Other Changes + * fix(parsing): ignore empty metadata ([2671330](https://github.com/lmnt-com/lmnt-python/commit/267133097ede5f0980c2479f41d2b1627baa9205)) + * fix(parsing): parse extra field types ([e652a62](https://github.com/lmnt-com/lmnt-python/commit/e652a624a99ba3c5c198aa00a198b6d0a4f69283)) + * chore(project): add settings file for vscode ([9d450b1](https://github.com/lmnt-com/lmnt-python/commit/9d450b1915dbb53698e1aec84b8fee4d485fee86)) + * feat(api): api update ([f0ef880](https://github.com/lmnt-com/lmnt-python/commit/f0ef8801046aa0ccad2eb44e67a6a44541d3f09b)) + July 17, 2025 - **BREAKING CHANGES**: The new v2 SDK provides more streaming functionality, a more modern, type-safe interface with better error handling, and improved performance. To migrate from the legacy v1 SDK, please update your code to use the new behavior or pin to a previous version if preferred. More details in the [migration guide](./MIGRATING.md). diff --git a/pyproject.toml b/pyproject.toml index f5ae058..ac12578 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lmnt" -version = "2.0.0" +version = "2.1.0" description = "The official Python library for the LMNT API" dynamic = ["readme"] license = "Apache-2.0" From e3ed4b024d7fb86a1f846d547cf978f2416ecbb7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 31 Jul 2025 06:38:34 +0000 Subject: [PATCH 8/9] feat(client): support file upload requests --- src/lmnt/_base_client.py | 5 ++++- src/lmnt/_files.py | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/lmnt/_base_client.py b/src/lmnt/_base_client.py index 7974f3a..8dc1eb6 100644 --- a/src/lmnt/_base_client.py +++ b/src/lmnt/_base_client.py @@ -532,7 +532,10 @@ def _build_request( is_body_allowed = options.method.lower() != "get" if is_body_allowed: - kwargs["json"] = json_data if is_given(json_data) else None + if isinstance(json_data, bytes): + kwargs["content"] = json_data + else: + kwargs["json"] = json_data if is_given(json_data) else None kwargs["files"] = files else: headers.pop("Content-Type", None) diff --git a/src/lmnt/_files.py b/src/lmnt/_files.py index 8c87e9b..c201a5c 100644 --- a/src/lmnt/_files.py +++ b/src/lmnt/_files.py @@ -69,12 +69,12 @@ def _transform_file(file: FileTypes) -> HttpxFileTypes: return file if is_tuple_t(file): - return (file[0], _read_file_content(file[1]), *file[2:]) + return (file[0], read_file_content(file[1]), *file[2:]) raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple") -def _read_file_content(file: FileContent) -> HttpxFileContent: +def read_file_content(file: FileContent) -> HttpxFileContent: if isinstance(file, os.PathLike): return pathlib.Path(file).read_bytes() return file @@ -111,12 +111,12 @@ async def _async_transform_file(file: FileTypes) -> HttpxFileTypes: return file if is_tuple_t(file): - return (file[0], await _async_read_file_content(file[1]), *file[2:]) + return (file[0], await async_read_file_content(file[1]), *file[2:]) raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple") -async def _async_read_file_content(file: FileContent) -> HttpxFileContent: +async def async_read_file_content(file: FileContent) -> HttpxFileContent: if isinstance(file, os.PathLike): return await anyio.Path(file).read_bytes() From adc3fac8a18f59605a177c1c86c0b0cfb620390d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 1 Aug 2025 00:41:39 +0000 Subject: [PATCH 9/9] Update sync state to d24223e37eee604e0ccc9267e9097de3030a3197 --- .sync_state | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.sync_state b/.sync_state index 52b6da1..cc446ad 100644 --- a/.sync_state +++ b/.sync_state @@ -1 +1 @@ -71a92b7562550ecaa0bc41ef22decde882221414 +d24223e37eee604e0ccc9267e9097de3030a3197