diff --git a/.release-please-manifest.json b/.release-please-manifest.json index cce9d1c6..c523ce19 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.7.0" + ".": "1.8.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 860efd9c..63c6ed1e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 39 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/finch%2Ffinch-6a3981b3fb38fe3ebae980af27e09f31d22db3659699afdd61f336594209ce42.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/finch%2Ffinch-612b573cabcee39c562dc91f5b185e2a0bfdce3a262618f0098602af2199af67.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 419e5205..5b5d9caa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 1.8.0 (2024-10-23) + +Full Changelog: [v1.7.0...v1.8.0](https://github.com/Finch-API/finch-api-python/compare/v1.7.0...v1.8.0) + +### Features + +* **api:** api update ([#511](https://github.com/Finch-API/finch-api-python/issues/511)) ([fab3cd1](https://github.com/Finch-API/finch-api-python/commit/fab3cd169bc417bb143153dc0b8a56cdb4bf7282)) +* **api:** api update ([#513](https://github.com/Finch-API/finch-api-python/issues/513)) ([32e705f](https://github.com/Finch-API/finch-api-python/commit/32e705fd3d78b6c9047aae99bbaa4a79724d2730)) + ## 1.7.0 (2024-10-03) Full Changelog: [v1.6.0...v1.7.0](https://github.com/Finch-API/finch-api-python/compare/v1.6.0...v1.7.0) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 91fc889e..b9879f76 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,9 +2,13 @@ ### With Rye -We use [Rye](https://rye.astral.sh/) to manage dependencies so we highly recommend [installing it](https://rye.astral.sh/guide/installation/) as it will automatically provision a Python environment with the expected Python version. +We use [Rye](https://rye.astral.sh/) to manage dependencies because it will automatically provision a Python environment with the expected Python version. To set it up, run: -After installing Rye, you'll just have to run this command: +```sh +$ ./scripts/bootstrap +``` + +Or [install Rye manually](https://rye.astral.sh/guide/installation/) and run: ```sh $ rye sync --all-features @@ -39,17 +43,17 @@ modify the contents of the `src/finch/lib/` and `examples/` directories. All files in the `examples/` directory are not modified by the generator and can be freely edited or added to. -```bash +```py # add an example to examples/.py #!/usr/bin/env -S rye run python … ``` -``` -chmod +x examples/.py +```sh +$ chmod +x examples/.py # run the example against your api -./examples/.py +$ ./examples/.py ``` ## Using the repository from source @@ -58,8 +62,8 @@ If you’d like to use the repository from source, you can either install from g To install via git: -```bash -pip install git+ssh://git@github.com/Finch-API/finch-api-python.git +```sh +$ pip install git+ssh://git@github.com/Finch-API/finch-api-python.git ``` Alternatively, you can build from source and install the wheel file: @@ -68,29 +72,29 @@ Building this package will create two files in the `dist/` directory, a `.tar.gz To create a distributable version of the library, all you have to do is run this command: -```bash -rye build +```sh +$ rye build # or -python -m build +$ python -m build ``` Then to install: ```sh -pip install ./path-to-wheel-file.whl +$ pip install ./path-to-wheel-file.whl ``` ## Running tests Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests. -```bash +```sh # you will need npm installed -npx prism mock path/to/your/openapi.yml +$ npx prism mock path/to/your/openapi.yml ``` -```bash -rye run pytest +```sh +$ ./scripts/test ``` ## Linting and formatting @@ -100,14 +104,14 @@ This repository uses [ruff](https://github.com/astral-sh/ruff) and To lint: -```bash -rye run lint +```sh +$ ./scripts/lint ``` To format and fix all ruff issues automatically: -```bash -rye run format +```sh +$ ./scripts/format ``` ## Publishing and releases diff --git a/README.md b/README.md index 0f6ca09f..83f338a2 100644 --- a/README.md +++ b/README.md @@ -430,3 +430,7 @@ print(finch.__version__) ## Requirements Python 3.7 or higher. + +## Contributing + +See [the contributing documentation](./CONTRIBUTING.md). diff --git a/pyproject.toml b/pyproject.toml index e8d61188..a0f0e6d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "finch-api" -version = "1.7.0" +version = "1.8.0" description = "The official Python library for the Finch API" dynamic = ["readme"] license = "Apache-2.0" @@ -63,11 +63,11 @@ format = { chain = [ "format:ruff", "format:docs", "fix:ruff", + # run formatting again to fix any inconsistencies when imports are stripped + "format:ruff", ]} -"format:black" = "black ." "format:docs" = "python scripts/utils/ruffen-docs.py README.md api.md" "format:ruff" = "ruff format" -"format:isort" = "isort ." "lint" = { chain = [ "check:ruff", @@ -125,10 +125,6 @@ path = "README.md" pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)' replacement = '[\1](https://github.com/Finch-API/finch-api-python/tree/main/\g<2>)' -[tool.black] -line-length = 120 -target-version = ["py37"] - [tool.pytest.ini_options] testpaths = ["tests"] addopts = "--tb=short" diff --git a/requirements-dev.lock b/requirements-dev.lock index 53209e90..213a0e0e 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -80,7 +80,7 @@ pytz==2023.3.post1 # via dirty-equals respx==0.20.2 rich==13.7.1 -ruff==0.6.5 +ruff==0.6.9 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 5df3d614..9f0e0efc 100644 --- a/src/finch/_base_client.py +++ b/src/finch/_base_client.py @@ -144,6 +144,12 @@ def __init__( self.url = url self.params = params + @override + def __repr__(self) -> str: + if self.url: + return f"{self.__class__.__name__}(url={self.url})" + return f"{self.__class__.__name__}(params={self.params})" + class BasePage(GenericModel, Generic[_T]): """ @@ -690,7 +696,8 @@ def _calculate_retry_timeout( if retry_after is not None and 0 < retry_after <= 60: return retry_after - nb_retries = max_retries - remaining_retries + # Also cap retry count to 1000 to avoid any potential overflows with `pow` + nb_retries = min(max_retries - remaining_retries, 1000) # Apply exponential backoff, but not more than the max. sleep_seconds = min(INITIAL_RETRY_DELAY * pow(2.0, nb_retries), MAX_RETRY_DELAY) @@ -1583,7 +1590,7 @@ async def _request( except Exception as err: log.debug("Encountered Exception", exc_info=True) - if retries_taken > 0: + if remaining_retries > 0: return await self._retry_request( input_options, cast_to, diff --git a/src/finch/_client.py b/src/finch/_client.py index 38117c5d..457b2d33 100644 --- a/src/finch/_client.py +++ b/src/finch/_client.py @@ -159,7 +159,7 @@ def __init__( @property @override def qs(self) -> Querystring: - return Querystring(array_format="comma") + return Querystring(array_format="brackets") @property @override @@ -495,7 +495,7 @@ def __init__( @property @override def qs(self) -> Querystring: - return Querystring(array_format="comma") + return Querystring(array_format="brackets") @property @override diff --git a/src/finch/_legacy_response.py b/src/finch/_legacy_response.py index d422f536..739b2f93 100644 --- a/src/finch/_legacy_response.py +++ b/src/finch/_legacy_response.py @@ -251,6 +251,9 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: if cast_to == float: return cast(R, float(response.text)) + if cast_to == bool: + return cast(R, response.text.lower() == "true") + origin = get_origin(cast_to) or cast_to if inspect.isclass(origin) and issubclass(origin, HttpxBinaryResponseContent): diff --git a/src/finch/_response.py b/src/finch/_response.py index a458c651..d22f84dc 100644 --- a/src/finch/_response.py +++ b/src/finch/_response.py @@ -192,6 +192,9 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: if cast_to == float: return cast(R, float(response.text)) + if cast_to == bool: + return cast(R, response.text.lower() == "true") + origin = get_origin(cast_to) or cast_to # handle the legacy binary response case diff --git a/src/finch/_version.py b/src/finch/_version.py index 47b1a5aa..0aef9d07 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.7.0" # x-release-please-version +__version__ = "1.8.0" # x-release-please-version diff --git a/src/finch/types/account_update_event.py b/src/finch/types/account_update_event.py index 84e914d1..38dd0776 100644 --- a/src/finch/types/account_update_event.py +++ b/src/finch/types/account_update_event.py @@ -39,7 +39,6 @@ "AccountUpdateEventDataAuthenticationMethodSupportedFieldsPayStatementPayStatementsEarnings", "AccountUpdateEventDataAuthenticationMethodSupportedFieldsPayStatementPayStatementsEmployeeDeductions", "AccountUpdateEventDataAuthenticationMethodSupportedFieldsPayStatementPayStatementsEmployerContributions", - "AccountUpdateEventDataAuthenticationMethodSupportedFieldsPayStatementPayStatementsEmployerDeductions", "AccountUpdateEventDataAuthenticationMethodSupportedFieldsPayStatementPayStatementsTaxes", "AccountUpdateEventDataAuthenticationMethodSupportedFieldsPayment", "AccountUpdateEventDataAuthenticationMethodSupportedFieldsPaymentPayPeriod", @@ -302,14 +301,6 @@ class AccountUpdateEventDataAuthenticationMethodSupportedFieldsPayStatementPaySt name: Optional[bool] = None -class AccountUpdateEventDataAuthenticationMethodSupportedFieldsPayStatementPayStatementsEmployerDeductions(BaseModel): - amount: Optional[bool] = None - - currency: Optional[bool] = None - - name: Optional[bool] = None - - class AccountUpdateEventDataAuthenticationMethodSupportedFieldsPayStatementPayStatementsTaxes(BaseModel): amount: Optional[bool] = None @@ -335,11 +326,6 @@ class AccountUpdateEventDataAuthenticationMethodSupportedFieldsPayStatementPaySt AccountUpdateEventDataAuthenticationMethodSupportedFieldsPayStatementPayStatementsEmployerContributions ] = None - employer_deductions: Optional[ - AccountUpdateEventDataAuthenticationMethodSupportedFieldsPayStatementPayStatementsEmployerDeductions - ] = None - """[DEPRECATED] Use `employer_contributions` instead""" - gross_pay: Optional[bool] = None individual_id: Optional[bool] = None diff --git a/src/finch/types/connect/session_new_response.py b/src/finch/types/connect/session_new_response.py index 2faa96c3..cf343973 100644 --- a/src/finch/types/connect/session_new_response.py +++ b/src/finch/types/connect/session_new_response.py @@ -1,7 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - from ..._models import BaseModel __all__ = ["SessionNewResponse"] diff --git a/src/finch/types/connect/session_reauthenticate_response.py b/src/finch/types/connect/session_reauthenticate_response.py index 1f28765a..323ebf71 100644 --- a/src/finch/types/connect/session_reauthenticate_response.py +++ b/src/finch/types/connect/session_reauthenticate_response.py @@ -1,7 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - from ..._models import BaseModel __all__ = ["SessionReauthenticateResponse"] diff --git a/src/finch/types/disconnect_response.py b/src/finch/types/disconnect_response.py index d30ec36c..ae2efc6d 100644 --- a/src/finch/types/disconnect_response.py +++ b/src/finch/types/disconnect_response.py @@ -1,7 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - from .._models import BaseModel __all__ = ["DisconnectResponse"] diff --git a/src/finch/types/hris/benfit_contribution.py b/src/finch/types/hris/benfit_contribution.py index ca806127..9a07b032 100644 --- a/src/finch/types/hris/benfit_contribution.py +++ b/src/finch/types/hris/benfit_contribution.py @@ -1,7 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - from .benefit_contribution import BenefitContribution __all__ = ["BenfitContribution"] diff --git a/src/finch/types/hris/create_company_benefits_response.py b/src/finch/types/hris/create_company_benefits_response.py index 105cec00..176c024d 100644 --- a/src/finch/types/hris/create_company_benefits_response.py +++ b/src/finch/types/hris/create_company_benefits_response.py @@ -1,7 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - from ..._models import BaseModel __all__ = ["CreateCompanyBenefitsResponse"] diff --git a/src/finch/types/hris/update_company_benefit_response.py b/src/finch/types/hris/update_company_benefit_response.py index 9cc7e992..de9f25b0 100644 --- a/src/finch/types/hris/update_company_benefit_response.py +++ b/src/finch/types/hris/update_company_benefit_response.py @@ -1,7 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - from ..._models import BaseModel __all__ = ["UpdateCompanyBenefitResponse"] diff --git a/src/finch/types/jobs/automated_create_response.py b/src/finch/types/jobs/automated_create_response.py index cb3159b9..8ff5d991 100644 --- a/src/finch/types/jobs/automated_create_response.py +++ b/src/finch/types/jobs/automated_create_response.py @@ -1,7 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - from ..._models import BaseModel __all__ = ["AutomatedCreateResponse"] diff --git a/src/finch/types/provider.py b/src/finch/types/provider.py index 5cbb2c18..d34b3acb 100644 --- a/src/finch/types/provider.py +++ b/src/finch/types/provider.py @@ -36,7 +36,6 @@ "AuthenticationMethodSupportedFieldsPayStatementPayStatementsEarnings", "AuthenticationMethodSupportedFieldsPayStatementPayStatementsEmployeeDeductions", "AuthenticationMethodSupportedFieldsPayStatementPayStatementsEmployerContributions", - "AuthenticationMethodSupportedFieldsPayStatementPayStatementsEmployerDeductions", "AuthenticationMethodSupportedFieldsPayStatementPayStatementsTaxes", "AuthenticationMethodSupportedFieldsPayment", "AuthenticationMethodSupportedFieldsPaymentPayPeriod", @@ -297,14 +296,6 @@ class AuthenticationMethodSupportedFieldsPayStatementPayStatementsEmployerContri name: Optional[bool] = None -class AuthenticationMethodSupportedFieldsPayStatementPayStatementsEmployerDeductions(BaseModel): - amount: Optional[bool] = None - - currency: Optional[bool] = None - - name: Optional[bool] = None - - class AuthenticationMethodSupportedFieldsPayStatementPayStatementsTaxes(BaseModel): amount: Optional[bool] = None @@ -326,9 +317,6 @@ class AuthenticationMethodSupportedFieldsPayStatementPayStatements(BaseModel): AuthenticationMethodSupportedFieldsPayStatementPayStatementsEmployerContributions ] = None - employer_deductions: Optional[AuthenticationMethodSupportedFieldsPayStatementPayStatementsEmployerDeductions] = None - """[DEPRECATED] Use `employer_contributions` instead""" - gross_pay: Optional[bool] = None individual_id: Optional[bool] = None diff --git a/src/finch/types/sandbox/job_create_response.py b/src/finch/types/sandbox/job_create_response.py index 80bc795d..f8e39f16 100644 --- a/src/finch/types/sandbox/job_create_response.py +++ b/src/finch/types/sandbox/job_create_response.py @@ -1,7 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - from ..._models import BaseModel __all__ = ["JobCreateResponse"] diff --git a/src/finch/types/sandbox/payment_create_response.py b/src/finch/types/sandbox/payment_create_response.py index 6b31d415..028bbde5 100644 --- a/src/finch/types/sandbox/payment_create_response.py +++ b/src/finch/types/sandbox/payment_create_response.py @@ -1,7 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - from ..._models import BaseModel __all__ = ["PaymentCreateResponse"] diff --git a/tests/test_client.py b/tests/test_client.py index 7c5deac3..4ee66df5 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -10,6 +10,7 @@ import tracemalloc from typing import Any, Union, cast from unittest import mock +from typing_extensions import Literal import httpx import pytest @@ -809,6 +810,7 @@ class Model(BaseModel): [3, "", 0.5], [2, "", 0.5 * 2.0], [1, "", 0.5 * 4.0], + [-1100, "", 7.8], # test large number potentially overflowing ], ) @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) @@ -823,7 +825,14 @@ def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str @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: + @pytest.mark.parametrize("failure_mode", ["status", "exception"]) + def test_retries_taken( + self, + client: Finch, + failures_before_success: int, + failure_mode: Literal["status", "exception"], + respx_mock: MockRouter, + ) -> None: client = client.with_options(max_retries=4) nb_retries = 0 @@ -832,6 +841,8 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: nonlocal nb_retries if nb_retries < failures_before_success: nb_retries += 1 + if failure_mode == "exception": + raise RuntimeError("oops") return httpx.Response(500) return httpx.Response(200) @@ -1686,6 +1697,7 @@ class Model(BaseModel): [3, "", 0.5], [2, "", 0.5 * 2.0], [1, "", 0.5 * 4.0], + [-1100, "", 7.8], # test large number potentially overflowing ], ) @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) @@ -1702,8 +1714,13 @@ async def test_parse_retry_after_header(self, remaining_retries: int, retry_afte @mock.patch("finch._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) @pytest.mark.asyncio + @pytest.mark.parametrize("failure_mode", ["status", "exception"]) async def test_retries_taken( - self, async_client: AsyncFinch, failures_before_success: int, respx_mock: MockRouter + self, + async_client: AsyncFinch, + failures_before_success: int, + failure_mode: Literal["status", "exception"], + respx_mock: MockRouter, ) -> None: client = async_client.with_options(max_retries=4) @@ -1713,6 +1730,8 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: nonlocal nb_retries if nb_retries < failures_before_success: nb_retries += 1 + if failure_mode == "exception": + raise RuntimeError("oops") return httpx.Response(500) return httpx.Response(200) diff --git a/tests/test_legacy_response.py b/tests/test_legacy_response.py index 7712696c..c6ec76aa 100644 --- a/tests/test_legacy_response.py +++ b/tests/test_legacy_response.py @@ -32,6 +32,31 @@ def test_response_parse_mismatched_basemodel(client: Finch) -> None: response.parse(to=PydanticModel) +@pytest.mark.parametrize( + "content, expected", + [ + ("false", False), + ("true", True), + ("False", False), + ("True", True), + ("TrUe", True), + ("FalSe", False), + ], +) +def test_response_parse_bool(client: Finch, content: str, expected: bool) -> None: + response = LegacyAPIResponse( + raw=httpx.Response(200, content=content), + client=client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + result = response.parse(to=bool) + assert result is expected + + def test_response_parse_custom_stream(client: Finch) -> None: response = LegacyAPIResponse( raw=httpx.Response(200, content=b"foo"), diff --git a/tests/test_models.py b/tests/test_models.py index 786b717d..1fbb88d9 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -245,7 +245,7 @@ class Model(BaseModel): assert m.foo is True m = Model.construct(foo="CARD_HOLDER") - assert m.foo is "CARD_HOLDER" + assert m.foo == "CARD_HOLDER" m = Model.construct(foo={"bar": False}) assert isinstance(m.foo, Submodel1) diff --git a/tests/test_response.py b/tests/test_response.py index 49538799..8241ad99 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -190,6 +190,56 @@ async def test_async_response_parse_annotated_type(async_client: AsyncFinch) -> assert obj.bar == 2 +@pytest.mark.parametrize( + "content, expected", + [ + ("false", False), + ("true", True), + ("False", False), + ("True", True), + ("TrUe", True), + ("FalSe", False), + ], +) +def test_response_parse_bool(client: Finch, content: str, expected: bool) -> None: + response = APIResponse( + raw=httpx.Response(200, content=content), + client=client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + result = response.parse(to=bool) + assert result is expected + + +@pytest.mark.parametrize( + "content, expected", + [ + ("false", False), + ("true", True), + ("False", False), + ("True", True), + ("TrUe", True), + ("FalSe", False), + ], +) +async def test_async_response_parse_bool(client: AsyncFinch, content: str, expected: bool) -> None: + response = AsyncAPIResponse( + raw=httpx.Response(200, content=content), + client=client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + result = await response.parse(to=bool) + assert result is expected + + class OtherModel(BaseModel): a: str