diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 90eeef65..7c37fb19 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "4.8.0" + ".": "4.8.1" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 02cf9d94..6643d58e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 4.8.1 (2025-07-22) + +Full Changelog: [v4.8.0...v4.8.1](https://github.com/orbcorp/orb-python/compare/v4.8.0...v4.8.1) + +### Bug Fixes + +* **parsing:** parse extra field types ([4b3a543](https://github.com/orbcorp/orb-python/commit/4b3a54366880f6ff2955db1d3d9986af47d1e25a)) + ## 4.8.0 (2025-07-21) Full Changelog: [v4.7.0...v4.8.0](https://github.com/orbcorp/orb-python/compare/v4.7.0...v4.8.0) diff --git a/pyproject.toml b/pyproject.toml index 4a95d259..5c32f4a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "orb-billing" -version = "4.8.0" +version = "4.8.1" description = "The official Python library for the orb API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/orb/_models.py b/src/orb/_models.py index ffcbf67b..b8387ce9 100644 --- a/src/orb/_models.py +++ b/src/orb/_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/src/orb/_version.py b/src/orb/_version.py index 94848477..a6ab00ac 100644 --- a/src/orb/_version.py +++ b/src/orb/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "orb" -__version__ = "4.8.0" # x-release-please-version +__version__ = "4.8.1" # x-release-please-version diff --git a/tests/test_models.py b/tests/test_models.py index 6a744074..dbf441e1 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"