Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
.prism.log
.vscode
_dev

__pycache__
Expand Down
4 changes: 2 additions & 2 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion .sync_state
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1d2c8905b08c612264d01f337870cd7465ad546e
d24223e37eee604e0ccc9267e9097de3030a3197
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"python.analysis.importFormat": "relative",
}
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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).

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
5 changes: 4 additions & 1 deletion src/lmnt/_base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 4 additions & 4 deletions src/lmnt/_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand Down
27 changes: 24 additions & 3 deletions src/lmnt/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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_):
Expand Down Expand Up @@ -439,7 +460,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:]
Expand Down
12 changes: 12 additions & 0 deletions src/lmnt/resources/speech.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ def convert(
format: Literal["aac", "mp3", "raw", "ulaw", "wav", "webm"] | NotGiven = NOT_GIVEN,
language: Literal[
"auto",
"ar",
"de",
"en",
"es",
Expand All @@ -78,6 +79,7 @@ def convert(
"th",
"tr",
"uk",
"ur",
"vi",
"zh",
]
Expand Down Expand Up @@ -163,6 +165,7 @@ def generate(
format: Literal["aac", "mp3", "raw", "ulaw", "wav", "webm"] | NotGiven = NOT_GIVEN,
language: Literal[
"auto",
"ar",
"de",
"en",
"es",
Expand All @@ -180,6 +183,7 @@ def generate(
"th",
"tr",
"uk",
"ur",
"vi",
"zh",
]
Expand Down Expand Up @@ -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",
Expand All @@ -303,6 +308,7 @@ def generate_detailed(
"th",
"tr",
"uk",
"ur",
"vi",
"zh",
]
Expand Down Expand Up @@ -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",
Expand All @@ -452,6 +459,7 @@ async def convert(
"th",
"tr",
"uk",
"ur",
"vi",
"zh",
]
Expand Down Expand Up @@ -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",
Expand All @@ -554,6 +563,7 @@ async def generate(
"th",
"tr",
"uk",
"ur",
"vi",
"zh",
]
Expand Down Expand Up @@ -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",
Expand All @@ -677,6 +688,7 @@ async def generate_detailed(
"th",
"tr",
"uk",
"ur",
"vi",
"zh",
]
Expand Down
2 changes: 2 additions & 0 deletions src/lmnt/types/speech_convert_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class SpeechConvertParams(TypedDict, total=False):

language: Literal[
"auto",
"ar",
"de",
"en",
"es",
Expand All @@ -63,6 +64,7 @@ class SpeechConvertParams(TypedDict, total=False):
"th",
"tr",
"uk",
"ur",
"vi",
"zh",
]
Expand Down
2 changes: 2 additions & 0 deletions src/lmnt/types/speech_generate_detailed_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class SpeechGenerateDetailedParams(TypedDict, total=False):

language: Literal[
"auto",
"ar",
"de",
"en",
"es",
Expand All @@ -57,6 +58,7 @@ class SpeechGenerateDetailedParams(TypedDict, total=False):
"th",
"tr",
"uk",
"ur",
"vi",
"zh",
]
Expand Down
2 changes: 2 additions & 0 deletions src/lmnt/types/speech_generate_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class SpeechGenerateParams(TypedDict, total=False):

language: Literal[
"auto",
"ar",
"de",
"en",
"es",
Expand All @@ -57,6 +58,7 @@ class SpeechGenerateParams(TypedDict, total=False):
"th",
"tr",
"uk",
"ur",
"vi",
"zh",
]
Expand Down
29 changes: 28 additions & 1 deletion tests/test_models.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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"
Loading