diff --git a/.release-please-manifest.json b/.release-please-manifest.json index fea34540..2601677b 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.0.0" + ".": "1.1.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 78627431..8e14d6fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## 1.1.0 (2024-08-07) + +Full Changelog: [v1.0.0...v1.1.0](https://github.com/Finch-API/finch-api-python/compare/v1.0.0...v1.1.0) + +### Features + +* **client:** add `retries_taken` to raw response class ([#462](https://github.com/Finch-API/finch-api-python/issues/462)) ([b62b180](https://github.com/Finch-API/finch-api-python/commit/b62b1809ac2c111db20af9b117409a2b4473997e)) + + +### Bug Fixes + +* **client:** correctly serialise array body params ([#466](https://github.com/Finch-API/finch-api-python/issues/466)) ([f94c81e](https://github.com/Finch-API/finch-api-python/commit/f94c81e1ffb396afdfbd7635b348adfd586cc038)) + + +### Chores + +* **internal:** bump pyright ([#461](https://github.com/Finch-API/finch-api-python/issues/461)) ([113a133](https://github.com/Finch-API/finch-api-python/commit/113a133eedd14d1bd579b710d41a7ea51a75f603)) +* **internal:** bump ruff version ([#464](https://github.com/Finch-API/finch-api-python/issues/464)) ([c440bcd](https://github.com/Finch-API/finch-api-python/commit/c440bcde1c636136baa2554b3e8ded69f604afdb)) +* **internal:** test updates ([#463](https://github.com/Finch-API/finch-api-python/issues/463)) ([19c58e6](https://github.com/Finch-API/finch-api-python/commit/19c58e6a2b1ec2a4cc3c103e911236a4f10af687)) +* **internal:** update pydantic compat helper function ([#465](https://github.com/Finch-API/finch-api-python/issues/465)) ([8656afe](https://github.com/Finch-API/finch-api-python/commit/8656afe878f85412712d85d52aa58068321ff792)) +* **internal:** use `TypeAlias` marker for type assignments ([#459](https://github.com/Finch-API/finch-api-python/issues/459)) ([3c0445a](https://github.com/Finch-API/finch-api-python/commit/3c0445a107202b107fec3ee73226f576824d8b40)) + ## 1.0.0 (2024-08-01) Full Changelog: [v0.23.1...v1.0.0](https://github.com/Finch-API/finch-api-python/compare/v0.23.1...v1.0.0) diff --git a/pyproject.toml b/pyproject.toml index 1066adcb..6cdc0b5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "finch-api" -version = "1.0.0" +version = "1.1.0" description = "The official Python library for the Finch API" dynamic = ["readme"] license = "Apache-2.0" @@ -77,8 +77,8 @@ format = { chain = [ "check:ruff", "typecheck", ]} -"check:ruff" = "ruff ." -"fix:ruff" = "ruff --fix ." +"check:ruff" = "ruff check ." +"fix:ruff" = "ruff check --fix ." typecheck = { chain = [ "typecheck:pyright", @@ -162,6 +162,11 @@ reportPrivateUsage = false line-length = 120 output-format = "grouped" target-version = "py37" + +[tool.ruff.format] +docstring-code-format = true + +[tool.ruff.lint] select = [ # isort "I", @@ -192,9 +197,6 @@ unfixable = [ ] ignore-init-module-imports = true -[tool.ruff.format] -docstring-code-format = true - [tool.ruff.lint.flake8-tidy-imports.banned-api] "functools.lru_cache".msg = "This function does not retain type information for the wrapped function's arguments; The `lru_cache` function from `_utils` should be used instead" diff --git a/requirements-dev.lock b/requirements-dev.lock index 9662d306..fefea7e3 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -70,7 +70,7 @@ pydantic-core==2.18.2 # via pydantic pygments==2.18.0 # via rich -pyright==1.1.364 +pyright==1.1.374 pytest==7.1.1 # via pytest-asyncio pytest-asyncio==0.21.1 @@ -80,7 +80,7 @@ pytz==2023.3.post1 # via dirty-equals respx==0.20.2 rich==13.7.1 -ruff==0.1.9 +ruff==0.5.6 setuptools==68.2.2 # via nodeenv six==1.16.0 diff --git a/src/finch/_base_client.py b/src/finch/_base_client.py index b72bb8dd..afcd47f5 100644 --- a/src/finch/_base_client.py +++ b/src/finch/_base_client.py @@ -125,16 +125,14 @@ def __init__( self, *, url: URL, - ) -> None: - ... + ) -> None: ... @overload def __init__( self, *, params: Query, - ) -> None: - ... + ) -> None: ... def __init__( self, @@ -167,8 +165,7 @@ def has_next_page(self) -> bool: return False return self.next_page_info() is not None - def next_page_info(self) -> Optional[PageInfo]: - ... + def next_page_info(self) -> Optional[PageInfo]: ... def _get_page_items(self) -> Iterable[_T]: # type: ignore[empty-body] ... @@ -904,8 +901,7 @@ def request( *, stream: Literal[True], stream_cls: Type[_StreamT], - ) -> _StreamT: - ... + ) -> _StreamT: ... @overload def request( @@ -915,8 +911,7 @@ def request( remaining_retries: Optional[int] = None, *, stream: Literal[False] = False, - ) -> ResponseT: - ... + ) -> ResponseT: ... @overload def request( @@ -927,8 +922,7 @@ def request( *, stream: bool = False, stream_cls: Type[_StreamT] | None = None, - ) -> ResponseT | _StreamT: - ... + ) -> ResponseT | _StreamT: ... def request( self, @@ -1050,6 +1044,7 @@ def _request( response=response, stream=stream, stream_cls=stream_cls, + retries_taken=options.get_max_retries(self.max_retries) - retries, ) def _retry_request( @@ -1091,6 +1086,7 @@ def _process_response( response: httpx.Response, stream: bool, stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, + retries_taken: int = 0, ) -> ResponseT: if response.request.headers.get(RAW_RESPONSE_HEADER) == "true": return cast( @@ -1102,6 +1098,7 @@ def _process_response( stream=stream, stream_cls=stream_cls, options=options, + retries_taken=retries_taken, ), ) @@ -1121,6 +1118,7 @@ def _process_response( stream=stream, stream_cls=stream_cls, options=options, + retries_taken=retries_taken, ), ) @@ -1134,6 +1132,7 @@ def _process_response( stream=stream, stream_cls=stream_cls, options=options, + retries_taken=retries_taken, ) if bool(response.request.headers.get(RAW_RESPONSE_HEADER)): return cast(ResponseT, api_response) @@ -1166,8 +1165,7 @@ def get( cast_to: Type[ResponseT], options: RequestOptions = {}, stream: Literal[False] = False, - ) -> ResponseT: - ... + ) -> ResponseT: ... @overload def get( @@ -1178,8 +1176,7 @@ def get( options: RequestOptions = {}, stream: Literal[True], stream_cls: type[_StreamT], - ) -> _StreamT: - ... + ) -> _StreamT: ... @overload def get( @@ -1190,8 +1187,7 @@ def get( options: RequestOptions = {}, stream: bool, stream_cls: type[_StreamT] | None = None, - ) -> ResponseT | _StreamT: - ... + ) -> ResponseT | _StreamT: ... def get( self, @@ -1217,8 +1213,7 @@ def post( options: RequestOptions = {}, files: RequestFiles | None = None, stream: Literal[False] = False, - ) -> ResponseT: - ... + ) -> ResponseT: ... @overload def post( @@ -1231,8 +1226,7 @@ def post( files: RequestFiles | None = None, stream: Literal[True], stream_cls: type[_StreamT], - ) -> _StreamT: - ... + ) -> _StreamT: ... @overload def post( @@ -1245,8 +1239,7 @@ def post( files: RequestFiles | None = None, stream: bool, stream_cls: type[_StreamT] | None = None, - ) -> ResponseT | _StreamT: - ... + ) -> ResponseT | _StreamT: ... def post( self, @@ -1479,8 +1472,7 @@ async def request( *, stream: Literal[False] = False, remaining_retries: Optional[int] = None, - ) -> ResponseT: - ... + ) -> ResponseT: ... @overload async def request( @@ -1491,8 +1483,7 @@ async def request( stream: Literal[True], stream_cls: type[_AsyncStreamT], remaining_retries: Optional[int] = None, - ) -> _AsyncStreamT: - ... + ) -> _AsyncStreamT: ... @overload async def request( @@ -1503,8 +1494,7 @@ async def request( stream: bool, stream_cls: type[_AsyncStreamT] | None = None, remaining_retries: Optional[int] = None, - ) -> ResponseT | _AsyncStreamT: - ... + ) -> ResponseT | _AsyncStreamT: ... async def request( self, @@ -1624,6 +1614,7 @@ async def _request( response=response, stream=stream, stream_cls=stream_cls, + retries_taken=options.get_max_retries(self.max_retries) - retries, ) async def _retry_request( @@ -1663,6 +1654,7 @@ async def _process_response( response: httpx.Response, stream: bool, stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, + retries_taken: int = 0, ) -> ResponseT: if response.request.headers.get(RAW_RESPONSE_HEADER) == "true": return cast( @@ -1674,6 +1666,7 @@ async def _process_response( stream=stream, stream_cls=stream_cls, options=options, + retries_taken=retries_taken, ), ) @@ -1693,6 +1686,7 @@ async def _process_response( stream=stream, stream_cls=stream_cls, options=options, + retries_taken=retries_taken, ), ) @@ -1706,6 +1700,7 @@ async def _process_response( stream=stream, stream_cls=stream_cls, options=options, + retries_taken=retries_taken, ) if bool(response.request.headers.get(RAW_RESPONSE_HEADER)): return cast(ResponseT, api_response) @@ -1728,8 +1723,7 @@ async def get( cast_to: Type[ResponseT], options: RequestOptions = {}, stream: Literal[False] = False, - ) -> ResponseT: - ... + ) -> ResponseT: ... @overload async def get( @@ -1740,8 +1734,7 @@ async def get( options: RequestOptions = {}, stream: Literal[True], stream_cls: type[_AsyncStreamT], - ) -> _AsyncStreamT: - ... + ) -> _AsyncStreamT: ... @overload async def get( @@ -1752,8 +1745,7 @@ async def get( options: RequestOptions = {}, stream: bool, stream_cls: type[_AsyncStreamT] | None = None, - ) -> ResponseT | _AsyncStreamT: - ... + ) -> ResponseT | _AsyncStreamT: ... async def get( self, @@ -1777,8 +1769,7 @@ async def post( files: RequestFiles | None = None, options: RequestOptions = {}, stream: Literal[False] = False, - ) -> ResponseT: - ... + ) -> ResponseT: ... @overload async def post( @@ -1791,8 +1782,7 @@ async def post( options: RequestOptions = {}, stream: Literal[True], stream_cls: type[_AsyncStreamT], - ) -> _AsyncStreamT: - ... + ) -> _AsyncStreamT: ... @overload async def post( @@ -1805,8 +1795,7 @@ async def post( options: RequestOptions = {}, stream: bool, stream_cls: type[_AsyncStreamT] | None = None, - ) -> ResponseT | _AsyncStreamT: - ... + ) -> ResponseT | _AsyncStreamT: ... async def post( self, diff --git a/src/finch/_compat.py b/src/finch/_compat.py index c919b5ad..21fe6941 100644 --- a/src/finch/_compat.py +++ b/src/finch/_compat.py @@ -7,7 +7,7 @@ import pydantic from pydantic.fields import FieldInfo -from ._types import StrBytesIntFloat +from ._types import IncEx, StrBytesIntFloat _T = TypeVar("_T") _ModelT = TypeVar("_ModelT", bound=pydantic.BaseModel) @@ -133,17 +133,20 @@ def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str: def model_dump( model: pydantic.BaseModel, *, + exclude: IncEx = None, exclude_unset: bool = False, exclude_defaults: bool = False, ) -> dict[str, Any]: if PYDANTIC_V2: return model.model_dump( + exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, ) return cast( "dict[str, Any]", model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast] + exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, ), @@ -159,22 +162,19 @@ def model_parse(model: type[_ModelT], data: Any) -> _ModelT: # generic models if TYPE_CHECKING: - class GenericModel(pydantic.BaseModel): - ... + class GenericModel(pydantic.BaseModel): ... else: if PYDANTIC_V2: # there no longer needs to be a distinction in v2 but # we still have to create our own subclass to avoid # inconsistent MRO ordering errors - class GenericModel(pydantic.BaseModel): - ... + class GenericModel(pydantic.BaseModel): ... else: import pydantic.generics - class GenericModel(pydantic.generics.GenericModel, pydantic.BaseModel): - ... + class GenericModel(pydantic.generics.GenericModel, pydantic.BaseModel): ... # cached properties @@ -193,26 +193,21 @@ class typed_cached_property(Generic[_T]): func: Callable[[Any], _T] attrname: str | None - def __init__(self, func: Callable[[Any], _T]) -> None: - ... + def __init__(self, func: Callable[[Any], _T]) -> None: ... @overload - def __get__(self, instance: None, owner: type[Any] | None = None) -> Self: - ... + def __get__(self, instance: None, owner: type[Any] | None = None) -> Self: ... @overload - def __get__(self, instance: object, owner: type[Any] | None = None) -> _T: - ... + def __get__(self, instance: object, owner: type[Any] | None = None) -> _T: ... def __get__(self, instance: object, owner: type[Any] | None = None) -> _T | Self: raise NotImplementedError() - def __set_name__(self, owner: type[Any], name: str) -> None: - ... + def __set_name__(self, owner: type[Any], name: str) -> None: ... # __set__ is not defined at runtime, but @cached_property is designed to be settable - def __set__(self, instance: object, value: _T) -> None: - ... + def __set__(self, instance: object, value: _T) -> None: ... else: try: from functools import cached_property as cached_property diff --git a/src/finch/_files.py b/src/finch/_files.py index 0d2022ae..715cc207 100644 --- a/src/finch/_files.py +++ b/src/finch/_files.py @@ -39,13 +39,11 @@ def assert_is_file_content(obj: object, *, key: str | None = None) -> None: @overload -def to_httpx_files(files: None) -> None: - ... +def to_httpx_files(files: None) -> None: ... @overload -def to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: - ... +def to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: ... def to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles | None: @@ -83,13 +81,11 @@ def _read_file_content(file: FileContent) -> HttpxFileContent: @overload -async def async_to_httpx_files(files: None) -> None: - ... +async def async_to_httpx_files(files: None) -> None: ... @overload -async def async_to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: - ... +async def async_to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: ... async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles | None: diff --git a/src/finch/_legacy_response.py b/src/finch/_legacy_response.py index c3911ba8..d422f536 100644 --- a/src/finch/_legacy_response.py +++ b/src/finch/_legacy_response.py @@ -5,7 +5,18 @@ import logging import datetime import functools -from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, Iterator, AsyncIterator, cast, overload +from typing import ( + TYPE_CHECKING, + Any, + Union, + Generic, + TypeVar, + Callable, + Iterator, + AsyncIterator, + cast, + overload, +) from typing_extensions import Awaitable, ParamSpec, override, deprecated, get_origin import anyio @@ -53,6 +64,9 @@ class LegacyAPIResponse(Generic[R]): http_response: httpx.Response + retries_taken: int + """The number of retries made. If no retries happened this will be `0`""" + def __init__( self, *, @@ -62,6 +76,7 @@ def __init__( stream: bool, stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, options: FinalRequestOptions, + retries_taken: int = 0, ) -> None: self._cast_to = cast_to self._client = client @@ -70,14 +85,13 @@ def __init__( self._stream_cls = stream_cls self._options = options self.http_response = raw + self.retries_taken = retries_taken @overload - def parse(self, *, to: type[_T]) -> _T: - ... + def parse(self, *, to: type[_T]) -> _T: ... @overload - def parse(self) -> R: - ... + def parse(self) -> R: ... def parse(self, *, to: type[_T] | None = None) -> R | _T: """Returns the rich python representation of this response's data. diff --git a/src/finch/_response.py b/src/finch/_response.py index df9358c9..a458c651 100644 --- a/src/finch/_response.py +++ b/src/finch/_response.py @@ -55,6 +55,9 @@ class BaseAPIResponse(Generic[R]): http_response: httpx.Response + retries_taken: int + """The number of retries made. If no retries happened this will be `0`""" + def __init__( self, *, @@ -64,6 +67,7 @@ def __init__( stream: bool, stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, options: FinalRequestOptions, + retries_taken: int = 0, ) -> None: self._cast_to = cast_to self._client = client @@ -72,6 +76,7 @@ def __init__( self._stream_cls = stream_cls self._options = options self.http_response = raw + self.retries_taken = retries_taken @property def headers(self) -> httpx.Headers: @@ -259,12 +264,10 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: class APIResponse(BaseAPIResponse[R]): @overload - def parse(self, *, to: type[_T]) -> _T: - ... + def parse(self, *, to: type[_T]) -> _T: ... @overload - def parse(self) -> R: - ... + def parse(self) -> R: ... def parse(self, *, to: type[_T] | None = None) -> R | _T: """Returns the rich python representation of this response's data. @@ -363,12 +366,10 @@ def iter_lines(self) -> Iterator[str]: class AsyncAPIResponse(BaseAPIResponse[R]): @overload - async def parse(self, *, to: type[_T]) -> _T: - ... + async def parse(self, *, to: type[_T]) -> _T: ... @overload - async def parse(self) -> R: - ... + async def parse(self) -> R: ... async def parse(self, *, to: type[_T] | None = None) -> R | _T: """Returns the rich python representation of this response's data. diff --git a/src/finch/_types.py b/src/finch/_types.py index 037cd3e9..b1233fc5 100644 --- a/src/finch/_types.py +++ b/src/finch/_types.py @@ -112,8 +112,7 @@ class NotGiven: For example: ```py - def get(timeout: Union[int, NotGiven, None] = NotGiven()) -> Response: - ... + def get(timeout: Union[int, NotGiven, None] = NotGiven()) -> Response: ... get(timeout=1) # 1s timeout @@ -163,16 +162,14 @@ def build( *, response: Response, data: object, - ) -> _T: - ... + ) -> _T: ... Headers = Mapping[str, Union[str, Omit]] class HeadersLikeProtocol(Protocol): - def get(self, __key: str) -> str | None: - ... + def get(self, __key: str) -> str | None: ... HeadersLike = Union[Headers, HeadersLikeProtocol] diff --git a/src/finch/_utils/_proxy.py b/src/finch/_utils/_proxy.py index c46a62a6..ffd883e9 100644 --- a/src/finch/_utils/_proxy.py +++ b/src/finch/_utils/_proxy.py @@ -59,5 +59,4 @@ def __as_proxied__(self) -> T: return cast(T, self) @abstractmethod - def __load__(self) -> T: - ... + def __load__(self) -> T: ... diff --git a/src/finch/_utils/_reflection.py b/src/finch/_utils/_reflection.py index 9a53c7bd..89aa712a 100644 --- a/src/finch/_utils/_reflection.py +++ b/src/finch/_utils/_reflection.py @@ -34,7 +34,7 @@ def assert_signatures_in_sync( if custom_param.annotation != source_param.annotation: errors.append( - f"types for the `{name}` param are do not match; source={repr(source_param.annotation)} checking={repr(source_param.annotation)}" + f"types for the `{name}` param are do not match; source={repr(source_param.annotation)} checking={repr(custom_param.annotation)}" ) continue diff --git a/src/finch/_utils/_utils.py b/src/finch/_utils/_utils.py index 34797c29..2fc5a1c6 100644 --- a/src/finch/_utils/_utils.py +++ b/src/finch/_utils/_utils.py @@ -211,20 +211,17 @@ def required_args(*variants: Sequence[str]) -> Callable[[CallableT], CallableT]: Example usage: ```py @overload - def foo(*, a: str) -> str: - ... + def foo(*, a: str) -> str: ... @overload - def foo(*, b: bool) -> str: - ... + def foo(*, b: bool) -> str: ... # This enforces the same constraints that a static type checker would # i.e. that either a or b must be passed to the function @required_args(["a"], ["b"]) - def foo(*, a: str | None = None, b: bool | None = None) -> str: - ... + def foo(*, a: str | None = None, b: bool | None = None) -> str: ... ``` """ @@ -286,18 +283,15 @@ def wrapper(*args: object, **kwargs: object) -> object: @overload -def strip_not_given(obj: None) -> None: - ... +def strip_not_given(obj: None) -> None: ... @overload -def strip_not_given(obj: Mapping[_K, _V | NotGiven]) -> dict[_K, _V]: - ... +def strip_not_given(obj: Mapping[_K, _V | NotGiven]) -> dict[_K, _V]: ... @overload -def strip_not_given(obj: object) -> object: - ... +def strip_not_given(obj: object) -> object: ... def strip_not_given(obj: object | None) -> object: diff --git a/src/finch/_version.py b/src/finch/_version.py index 53d00296..5232e0ba 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.0.0" # x-release-please-version +__version__ = "1.1.0" # x-release-please-version diff --git a/src/finch/resources/hris/benefits/individuals.py b/src/finch/resources/hris/benefits/individuals.py index e49ef186..31d01a9f 100644 --- a/src/finch/resources/hris/benefits/individuals.py +++ b/src/finch/resources/hris/benefits/individuals.py @@ -71,7 +71,7 @@ def enroll_many( return self._get_api_list( f"/employer/benefits/{benefit_id}/individuals", page=SyncSinglePage[EnrolledIndividual], - body=maybe_transform(individuals, individual_enroll_many_params.IndividualEnrollManyParams), + body=maybe_transform(individuals, Iterable[individual_enroll_many_params.Individual]), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -243,7 +243,7 @@ def enroll_many( return self._get_api_list( f"/employer/benefits/{benefit_id}/individuals", page=AsyncSinglePage[EnrolledIndividual], - body=maybe_transform(individuals, individual_enroll_many_params.IndividualEnrollManyParams), + body=maybe_transform(individuals, Iterable[individual_enroll_many_params.Individual]), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/finch/resources/sandbox/directory.py b/src/finch/resources/sandbox/directory.py index d06a96c9..f6b414ad 100644 --- a/src/finch/resources/sandbox/directory.py +++ b/src/finch/resources/sandbox/directory.py @@ -59,7 +59,7 @@ def create( """ return self._post( "/sandbox/directory", - body=maybe_transform(body, directory_create_params.DirectoryCreateParams), + body=maybe_transform(body, Iterable[directory_create_params.Body]), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -104,7 +104,7 @@ async def create( """ return await self._post( "/sandbox/directory", - body=await async_maybe_transform(body, directory_create_params.DirectoryCreateParams), + body=await async_maybe_transform(body, Iterable[directory_create_params.Body]), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/finch/types/account_update_event.py b/src/finch/types/account_update_event.py index 01f3f967..84e914d1 100644 --- a/src/finch/types/account_update_event.py +++ b/src/finch/types/account_update_event.py @@ -323,9 +323,9 @@ class AccountUpdateEventDataAuthenticationMethodSupportedFieldsPayStatementPaySt class AccountUpdateEventDataAuthenticationMethodSupportedFieldsPayStatementPayStatements(BaseModel): - earnings: Optional[ - AccountUpdateEventDataAuthenticationMethodSupportedFieldsPayStatementPayStatementsEarnings - ] = None + earnings: Optional[AccountUpdateEventDataAuthenticationMethodSupportedFieldsPayStatementPayStatementsEarnings] = ( + None + ) employee_deductions: Optional[ AccountUpdateEventDataAuthenticationMethodSupportedFieldsPayStatementPayStatementsEmployeeDeductions diff --git a/src/finch/types/hris/benefit_frequency.py b/src/finch/types/hris/benefit_frequency.py index e1d56e94..82116577 100644 --- a/src/finch/types/hris/benefit_frequency.py +++ b/src/finch/types/hris/benefit_frequency.py @@ -1,8 +1,8 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import Optional -from typing_extensions import Literal +from typing_extensions import Literal, TypeAlias __all__ = ["BenefitFrequency"] -BenefitFrequency = Optional[Literal["one_time", "every_paycheck", "monthly"]] +BenefitFrequency: TypeAlias = Optional[Literal["one_time", "every_paycheck", "monthly"]] diff --git a/src/finch/types/hris/benefit_type.py b/src/finch/types/hris/benefit_type.py index d578f2f9..2c8ad7f4 100644 --- a/src/finch/types/hris/benefit_type.py +++ b/src/finch/types/hris/benefit_type.py @@ -1,11 +1,11 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import Optional -from typing_extensions import Literal +from typing_extensions import Literal, TypeAlias __all__ = ["BenefitType"] -BenefitType = Optional[ +BenefitType: TypeAlias = Optional[ Literal[ "401k", "401k_roth", diff --git a/src/finch/types/hris/benefits_support.py b/src/finch/types/hris/benefits_support.py index 4632a969..95d6669c 100644 --- a/src/finch/types/hris/benefits_support.py +++ b/src/finch/types/hris/benefits_support.py @@ -37,5 +37,4 @@ class BenefitsSupport(BaseModel): # Stub to indicate that arbitrary properties are accepted. # To access properties that are not valid identifiers you can use `getattr`, e.g. # `getattr(obj, '$type')` - def __getattr__(self, attr: str) -> Optional[BenefitFeaturesAndOperations]: - ... + def __getattr__(self, attr: str) -> Optional[BenefitFeaturesAndOperations]: ... diff --git a/src/finch/types/sandbox/directory_create_response.py b/src/finch/types/sandbox/directory_create_response.py index 1d639939..f02392cd 100644 --- a/src/finch/types/sandbox/directory_create_response.py +++ b/src/finch/types/sandbox/directory_create_response.py @@ -1,7 +1,8 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import List +from typing_extensions import TypeAlias __all__ = ["DirectoryCreateResponse"] -DirectoryCreateResponse = List[object] +DirectoryCreateResponse: TypeAlias = List[object] diff --git a/src/finch/types/sandbox/jobs/configuration_retrieve_response.py b/src/finch/types/sandbox/jobs/configuration_retrieve_response.py index 33fef2be..fce0b77d 100644 --- a/src/finch/types/sandbox/jobs/configuration_retrieve_response.py +++ b/src/finch/types/sandbox/jobs/configuration_retrieve_response.py @@ -1,9 +1,10 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import List +from typing_extensions import TypeAlias from .sandbox_job_configuration import SandboxJobConfiguration __all__ = ["ConfigurationRetrieveResponse"] -ConfigurationRetrieveResponse = List[SandboxJobConfiguration] +ConfigurationRetrieveResponse: TypeAlias = List[SandboxJobConfiguration] diff --git a/src/finch/types/shared/connection_status_type.py b/src/finch/types/shared/connection_status_type.py index 9021c0b7..9e845c0f 100644 --- a/src/finch/types/shared/connection_status_type.py +++ b/src/finch/types/shared/connection_status_type.py @@ -1,9 +1,9 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing_extensions import Literal +from typing_extensions import Literal, TypeAlias __all__ = ["ConnectionStatusType"] -ConnectionStatusType = Literal[ +ConnectionStatusType: TypeAlias = Literal[ "pending", "processing", "connected", "error_no_account_setup", "error_permissions", "reauth" ] diff --git a/src/finch/types/shared/operation_support.py b/src/finch/types/shared/operation_support.py index 10bda0ab..69640454 100644 --- a/src/finch/types/shared/operation_support.py +++ b/src/finch/types/shared/operation_support.py @@ -1,7 +1,9 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing_extensions import Literal +from typing_extensions import Literal, TypeAlias __all__ = ["OperationSupport"] -OperationSupport = Literal["supported", "not_supported_by_finch", "not_supported_by_provider", "client_access_only"] +OperationSupport: TypeAlias = Literal[ + "supported", "not_supported_by_finch", "not_supported_by_provider", "client_access_only" +] diff --git a/src/finch/types/shared_params/connection_status_type.py b/src/finch/types/shared_params/connection_status_type.py index d7caa1cf..4db153e7 100644 --- a/src/finch/types/shared_params/connection_status_type.py +++ b/src/finch/types/shared_params/connection_status_type.py @@ -2,10 +2,10 @@ from __future__ import annotations -from typing_extensions import Literal +from typing_extensions import Literal, TypeAlias __all__ = ["ConnectionStatusType"] -ConnectionStatusType = Literal[ +ConnectionStatusType: TypeAlias = Literal[ "pending", "processing", "connected", "error_no_account_setup", "error_permissions", "reauth" ] diff --git a/src/finch/types/webhook_event.py b/src/finch/types/webhook_event.py index 8d3788c2..17bb1d4f 100644 --- a/src/finch/types/webhook_event.py +++ b/src/finch/types/webhook_event.py @@ -1,6 +1,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import Union +from typing_extensions import TypeAlias from .company_event import CompanyEvent from .payment_event import PaymentEvent @@ -13,7 +14,7 @@ __all__ = ["WebhookEvent"] -WebhookEvent = Union[ +WebhookEvent: TypeAlias = Union[ AccountUpdateEvent, JobCompletionEvent, CompanyEvent, diff --git a/tests/test_client.py b/tests/test_client.py index 7863d7a1..8c43932a 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -36,6 +36,10 @@ def _get_params(client: BaseClient[Any, Any]) -> dict[str, str]: return dict(url.params) +def _low_retry_timeout(*_args: Any, **_kwargs: Any) -> float: + return 0.1 + + class TestFinch: client = Finch( base_url=base_url, @@ -950,6 +954,49 @@ def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str calculated = client._calculate_retry_timeout(remaining_retries, options, headers) assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType] + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("finch._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + def test_retries_taken(self, client: Finch, failures_before_success: int, respx_mock: MockRouter) -> None: + client = client.with_options(max_retries=4) + + nb_retries = 0 + + def retry_handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_retries + if nb_retries < failures_before_success: + nb_retries += 1 + return httpx.Response(500) + return httpx.Response(200) + + respx_mock.get("/employer/directory").mock(side_effect=retry_handler) + + response = client.hris.directory.with_raw_response.list() + + assert response.retries_taken == failures_before_success + + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("finch._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + def test_retries_taken_new_response_class( + self, client: Finch, failures_before_success: int, respx_mock: MockRouter + ) -> None: + client = client.with_options(max_retries=4) + + nb_retries = 0 + + def retry_handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_retries + if nb_retries < failures_before_success: + nb_retries += 1 + return httpx.Response(500) + return httpx.Response(200) + + respx_mock.get("/employer/directory").mock(side_effect=retry_handler) + + with client.hris.directory.with_streaming_response.list() as response: + assert response.retries_taken == failures_before_success + class TestAsyncFinch: client = AsyncFinch( @@ -1870,3 +1917,50 @@ async def test_parse_retry_after_header(self, remaining_retries: int, retry_afte options = FinalRequestOptions(method="get", url="/foo", max_retries=3) calculated = client._calculate_retry_timeout(remaining_retries, options, headers) assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType] + + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("finch._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + @pytest.mark.asyncio + async def test_retries_taken( + self, async_client: AsyncFinch, failures_before_success: int, respx_mock: MockRouter + ) -> None: + client = async_client.with_options(max_retries=4) + + nb_retries = 0 + + def retry_handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_retries + if nb_retries < failures_before_success: + nb_retries += 1 + return httpx.Response(500) + return httpx.Response(200) + + respx_mock.get("/employer/directory").mock(side_effect=retry_handler) + + response = await client.hris.directory.with_raw_response.list() + + assert response.retries_taken == failures_before_success + + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("finch._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + @pytest.mark.asyncio + async def test_retries_taken_new_response_class( + self, async_client: AsyncFinch, failures_before_success: int, respx_mock: MockRouter + ) -> None: + client = async_client.with_options(max_retries=4) + + nb_retries = 0 + + def retry_handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_retries + if nb_retries < failures_before_success: + nb_retries += 1 + return httpx.Response(500) + return httpx.Response(200) + + respx_mock.get("/employer/directory").mock(side_effect=retry_handler) + + async with client.hris.directory.with_streaming_response.list() as response: + assert response.retries_taken == failures_before_success diff --git a/tests/test_deepcopy.py b/tests/test_deepcopy.py index 3fce55d9..5fb88a94 100644 --- a/tests/test_deepcopy.py +++ b/tests/test_deepcopy.py @@ -41,8 +41,7 @@ def test_nested_list() -> None: assert_different_identities(obj1[1], obj2[1]) -class MyObject: - ... +class MyObject: ... def test_ignores_other_types() -> None: diff --git a/tests/test_legacy_response.py b/tests/test_legacy_response.py index 994448d4..7c3c0a3a 100644 --- a/tests/test_legacy_response.py +++ b/tests/test_legacy_response.py @@ -12,8 +12,7 @@ from finch._legacy_response import LegacyAPIResponse -class PydanticModel(pydantic.BaseModel): - ... +class PydanticModel(pydantic.BaseModel): ... def test_response_parse_mismatched_basemodel(client: Finch) -> None: diff --git a/tests/test_response.py b/tests/test_response.py index 3e70ad63..ea4de08d 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -19,16 +19,13 @@ from finch._base_client import FinalRequestOptions -class ConcreteBaseAPIResponse(APIResponse[bytes]): - ... +class ConcreteBaseAPIResponse(APIResponse[bytes]): ... -class ConcreteAPIResponse(APIResponse[List[str]]): - ... +class ConcreteAPIResponse(APIResponse[List[str]]): ... -class ConcreteAsyncAPIResponse(APIResponse[httpx.Response]): - ... +class ConcreteAsyncAPIResponse(APIResponse[httpx.Response]): ... def test_extract_response_type_direct_classes() -> None: @@ -56,8 +53,7 @@ def test_extract_response_type_binary_response() -> None: assert extract_response_type(AsyncBinaryAPIResponse) == bytes -class PydanticModel(pydantic.BaseModel): - ... +class PydanticModel(pydantic.BaseModel): ... def test_response_parse_mismatched_basemodel(client: Finch) -> None: diff --git a/tests/test_utils/test_typing.py b/tests/test_utils/test_typing.py index 55d73a39..38916e87 100644 --- a/tests/test_utils/test_typing.py +++ b/tests/test_utils/test_typing.py @@ -9,24 +9,19 @@ _T3 = TypeVar("_T3") -class BaseGeneric(Generic[_T]): - ... +class BaseGeneric(Generic[_T]): ... -class SubclassGeneric(BaseGeneric[_T]): - ... +class SubclassGeneric(BaseGeneric[_T]): ... -class BaseGenericMultipleTypeArgs(Generic[_T, _T2, _T3]): - ... +class BaseGenericMultipleTypeArgs(Generic[_T, _T2, _T3]): ... -class SubclassGenericMultipleTypeArgs(BaseGenericMultipleTypeArgs[_T, _T2, _T3]): - ... +class SubclassGenericMultipleTypeArgs(BaseGenericMultipleTypeArgs[_T, _T2, _T3]): ... -class SubclassDifferentOrderGenericMultipleTypeArgs(BaseGenericMultipleTypeArgs[_T2, _T, _T3]): - ... +class SubclassDifferentOrderGenericMultipleTypeArgs(BaseGenericMultipleTypeArgs[_T2, _T, _T3]): ... def test_extract_type_var() -> None: diff --git a/tests/utils.py b/tests/utils.py index 03639932..2af6d870 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -8,7 +8,7 @@ from datetime import date, datetime from typing_extensions import Literal, get_args, get_origin, assert_type -from finch._types import NoneType +from finch._types import Omit, NoneType from finch._utils import ( is_dict, is_list, @@ -139,11 +139,15 @@ def _assert_list_type(type_: type[object], value: object) -> None: @contextlib.contextmanager -def update_env(**new_env: str) -> Iterator[None]: +def update_env(**new_env: str | Omit) -> Iterator[None]: old = os.environ.copy() try: - os.environ.update(new_env) + for name, value in new_env.items(): + if isinstance(value, Omit): + os.environ.pop(name, None) + else: + os.environ[name] = value yield None finally: