diff --git a/.gitignore b/.gitignore index 87797408..95ceb189 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ .prism.log -.vscode _dev __pycache__ diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 6c6045ba..81d2de2d 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.30.3" + ".": "1.31.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index f1c7d32b..3fe8a845 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 46 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/finch%2Ffinch-f7e741bc6e0175fd96a9db5348092b90a77b0985154c0814bb681ad5dccdf19a.yml -openapi_spec_hash: b348a9ef407a8e91dd770fcb219d4ac5 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/finch%2Ffinch-73c284d36c1ed2d9963fc733e421005fad76e559de8efe9baa6511a43dd72668.yml +openapi_spec_hash: 1e58c4445919b71c77e5c7f16bd6fa7d config_hash: 5146b12344dae76238940989dac1e8a0 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..5b010307 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.analysis.importFormat": "relative", +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 0963c411..758775fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## 1.31.0 (2025-07-24) + +Full Changelog: [v1.30.3...v1.31.0](https://github.com/Finch-API/finch-api-python/compare/v1.30.3...v1.31.0) + +### Features + +* **api:** api update ([1dd1ffa](https://github.com/Finch-API/finch-api-python/commit/1dd1fface4ca9b49a92bc319b233b7689f23b006)) +* **api:** api update ([d0c7fd2](https://github.com/Finch-API/finch-api-python/commit/d0c7fd2ada777eba81f483ce11340c01aca1abe0)) + + +### Bug Fixes + +* **parsing:** ignore empty metadata ([acaec4b](https://github.com/Finch-API/finch-api-python/commit/acaec4b1fffcb4323e950cb393707724f2fa7e62)) +* **parsing:** parse extra field types ([d14c906](https://github.com/Finch-API/finch-api-python/commit/d14c9060175c9a1e345f5507a9634c6d81683395)) + + +### Chores + +* **project:** add settings file for vscode ([a2c4c2a](https://github.com/Finch-API/finch-api-python/commit/a2c4c2a8d81e8086dd8c8962806415f087b132c5)) + ## 1.30.3 (2025-07-11) Full Changelog: [v1.30.2...v1.30.3](https://github.com/Finch-API/finch-api-python/compare/v1.30.2...v1.30.3) diff --git a/pyproject.toml b/pyproject.toml index be6948bb..bc03d901 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "finch-api" -version = "1.30.3" +version = "1.31.0" description = "The official Python library for the Finch API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/finch/_models.py b/src/finch/_models.py index 528d5680..b8387ce9 100644 --- a/src/finch/_models.py +++ b/src/finch/_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_): @@ -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:] diff --git a/src/finch/_version.py b/src/finch/_version.py index f9e57b72..2c0bc814 100644 --- a/src/finch/_version.py +++ b/src/finch/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "finch" -__version__ = "1.30.3" # x-release-please-version +__version__ = "1.31.0" # x-release-please-version diff --git a/src/finch/types/hris/benefit_create_params.py b/src/finch/types/hris/benefit_create_params.py index c964336b..48b21cf3 100644 --- a/src/finch/types/hris/benefit_create_params.py +++ b/src/finch/types/hris/benefit_create_params.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Iterable, Optional -from typing_extensions import Literal, TypedDict +from typing_extensions import Literal, Required, TypedDict from .benefit_type import BenefitType from .benefit_frequency import BenefitFrequency @@ -30,12 +30,12 @@ class BenefitCreateParams(TypedDict, total=False): class CompanyContributionTier(TypedDict, total=False): - match: int + match: Required[int] - threshold: int + threshold: Required[int] class CompanyContribution(TypedDict, total=False): - tiers: Iterable[CompanyContributionTier] + tiers: Required[Iterable[CompanyContributionTier]] - type: Literal["match"] + type: Required[Literal["match"]] diff --git a/src/finch/types/hris/benefit_frequency.py b/src/finch/types/hris/benefit_frequency.py index 82116577..ed91d8b2 100644 --- a/src/finch/types/hris/benefit_frequency.py +++ b/src/finch/types/hris/benefit_frequency.py @@ -5,4 +5,4 @@ __all__ = ["BenefitFrequency"] -BenefitFrequency: TypeAlias = Optional[Literal["one_time", "every_paycheck", "monthly"]] +BenefitFrequency: TypeAlias = Optional[Literal["every_paycheck", "monthly", "one_time"]] diff --git a/src/finch/types/hris/benefits/individual_benefit.py b/src/finch/types/hris/benefits/individual_benefit.py index 9d4ff80a..f550fdc2 100644 --- a/src/finch/types/hris/benefits/individual_benefit.py +++ b/src/finch/types/hris/benefits/individual_benefit.py @@ -1,15 +1,15 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional -from typing_extensions import Literal +from typing import Union, Optional +from typing_extensions import Literal, TypeAlias from ...._models import BaseModel from ..benefit_contribution import BenefitContribution -__all__ = ["IndividualBenefit", "Body"] +__all__ = ["IndividualBenefit", "Body", "BodyUnionMember0", "BodyBatchError"] -class Body(BaseModel): +class BodyUnionMember0(BaseModel): annual_maximum: Optional[int] = None """ If the benefit supports annual maximum, the amount in cents for this individual. @@ -29,9 +29,22 @@ class Body(BaseModel): """Type for HSA contribution limit if the benefit is a HSA.""" +class BodyBatchError(BaseModel): + code: float + + message: str + + name: str + + finch_code: Optional[str] = None + + +Body: TypeAlias = Union[BodyUnionMember0, BodyBatchError] + + class IndividualBenefit(BaseModel): - body: Optional[Body] = None + body: Body - code: Optional[int] = None + code: int - individual_id: Optional[str] = None + individual_id: str diff --git a/src/finch/types/hris/benefits_support.py b/src/finch/types/hris/benefits_support.py index 95d6669c..99160dd4 100644 --- a/src/finch/types/hris/benefits_support.py +++ b/src/finch/types/hris/benefits_support.py @@ -1,6 +1,8 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Dict, Optional + +from pydantic import Field as FieldInfo from ..._models import BaseModel from .benefit_features_and_operations import BenefitFeaturesAndOperations @@ -33,6 +35,7 @@ class BenefitsSupport(BaseModel): simple_ira: Optional[BenefitFeaturesAndOperations] = None + __pydantic_extra__: Dict[str, Optional[BenefitFeaturesAndOperations]] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] if TYPE_CHECKING: # Stub to indicate that arbitrary properties are accepted. # To access properties that are not valid identifiers you can use `getattr`, e.g. diff --git a/src/finch/types/hris/company_benefit.py b/src/finch/types/hris/company_benefit.py index 6f0dbebf..eda65280 100644 --- a/src/finch/types/hris/company_benefit.py +++ b/src/finch/types/hris/company_benefit.py @@ -11,24 +11,21 @@ class CompanyContributionTier(BaseModel): - match: Optional[int] = None + match: int - threshold: Optional[int] = None + threshold: int class CompanyContribution(BaseModel): - tiers: Optional[List[CompanyContributionTier]] = None + tiers: List[CompanyContributionTier] - type: Optional[Literal["match"]] = None + type: Literal["match"] class CompanyBenefit(BaseModel): benefit_id: str """The id of the benefit.""" - company_contribution: Optional[CompanyContribution] = None - """The company match for this benefit.""" - description: Optional[str] = None frequency: Optional[BenefitFrequency] = None @@ -36,3 +33,6 @@ class CompanyBenefit(BaseModel): type: Optional[BenefitType] = None """Type of benefit.""" + + company_contribution: Optional[CompanyContribution] = None + """The company match for this benefit.""" diff --git a/src/finch/types/hris/supported_benefit.py b/src/finch/types/hris/supported_benefit.py index 08d94399..4b57eeb4 100644 --- a/src/finch/types/hris/supported_benefit.py +++ b/src/finch/types/hris/supported_benefit.py @@ -13,12 +13,6 @@ class SupportedBenefit(BaseModel): annual_maximum: Optional[bool] = None """Whether the provider supports an annual maximum for this benefit.""" - catch_up: Optional[bool] = None - """Whether the provider supports catch up for this benefit. - - This field will only be true for retirement benefits. - """ - company_contribution: Optional[List[Optional[Literal["fixed", "percent"]]]] = None """Supported contribution types. @@ -33,10 +27,16 @@ class SupportedBenefit(BaseModel): An empty array indicates deductions are not supported. """ - frequencies: Optional[List[Optional[BenefitFrequency]]] = None + frequencies: List[Optional[BenefitFrequency]] """The list of frequencies supported by the provider for this benefit""" - hsa_contribution_limit: Optional[List[Optional[Literal["individual", "family"]]]] = None + catch_up: Optional[bool] = None + """Whether the provider supports catch up for this benefit. + + This field will only be true for retirement benefits. + """ + + hsa_contribution_limit: Optional[List[Optional[Literal["family", "individual"]]]] = None """Whether the provider supports HSA contribution limits. Empty if this feature is not supported for the benefit. This array only has diff --git a/src/finch/types/jobs/automated_create_response.py b/src/finch/types/jobs/automated_create_response.py index 9d53fb1c..29d9e418 100644 --- a/src/finch/types/jobs/automated_create_response.py +++ b/src/finch/types/jobs/automated_create_response.py @@ -1,5 +1,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from typing import Optional + from ..._models import BaseModel __all__ = ["AutomatedCreateResponse"] @@ -9,11 +11,14 @@ class AutomatedCreateResponse(BaseModel): allowed_refreshes: int """The number of allowed refreshes per hour (per hour, fixed window)""" - job_id: str + remaining_refreshes: int + """The number of remaining refreshes available (per hour, fixed window)""" + + job_id: Optional[str] = None """The id of the job that has been created.""" - job_url: str + job_url: Optional[str] = None """The url that can be used to retrieve the job status""" - remaining_refreshes: int - """The number of remaining refreshes available (per hour, fixed window)""" + retry_at: Optional[str] = None + """ISO 8601 timestamp indicating when to retry the request""" diff --git a/tests/api_resources/hris/test_benefits.py b/tests/api_resources/hris/test_benefits.py index 7961a951..a9fe0954 100644 --- a/tests/api_resources/hris/test_benefits.py +++ b/tests/api_resources/hris/test_benefits.py @@ -41,7 +41,7 @@ def test_method_create_with_all_params(self, client: Finch) -> None: "type": "match", }, description="description", - frequency="one_time", + frequency="every_paycheck", type="457", ) assert_matches_type(CreateCompanyBenefitsResponse, benefit, path=["response"]) @@ -224,7 +224,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncFinch) -> "type": "match", }, description="description", - frequency="one_time", + frequency="every_paycheck", type="457", ) assert_matches_type(CreateCompanyBenefitsResponse, benefit, path=["response"]) diff --git a/tests/test_models.py b/tests/test_models.py index b906887e..16088dec 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"