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
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "1.30.3"
".": "1.31.0"
}
4 changes: 2 additions & 2 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -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
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",
}
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
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 = "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"
Expand Down
27 changes: 24 additions & 3 deletions src/finch/_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
2 changes: 1 addition & 1 deletion src/finch/_version.py
Original file line number Diff line number Diff line change
@@ -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
10 changes: 5 additions & 5 deletions src/finch/types/hris/benefit_create_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"]]
2 changes: 1 addition & 1 deletion src/finch/types/hris/benefit_frequency.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@

__all__ = ["BenefitFrequency"]

BenefitFrequency: TypeAlias = Optional[Literal["one_time", "every_paycheck", "monthly"]]
BenefitFrequency: TypeAlias = Optional[Literal["every_paycheck", "monthly", "one_time"]]
27 changes: 20 additions & 7 deletions src/finch/types/hris/benefits/individual_benefit.py
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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
5 changes: 4 additions & 1 deletion src/finch/types/hris/benefits_support.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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.
Expand Down
14 changes: 7 additions & 7 deletions src/finch/types/hris/company_benefit.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,28 @@


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
"""The frequency of the benefit deduction/contribution."""

type: Optional[BenefitType] = None
"""Type of benefit."""

company_contribution: Optional[CompanyContribution] = None
"""The company match for this benefit."""
16 changes: 8 additions & 8 deletions src/finch/types/hris/supported_benefit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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
Expand Down
13 changes: 9 additions & 4 deletions src/finch/types/jobs/automated_create_response.py
Original file line number Diff line number Diff line change
@@ -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"]
Expand All @@ -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"""
4 changes: 2 additions & 2 deletions tests/api_resources/hris/test_benefits.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Expand Down Expand Up @@ -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"])
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"